diff --git a/src/vkcv/CommandStreamManager.cpp b/src/vkcv/CommandStreamManager.cpp
index 872e91c54b1b64616ad5867926e6f656f44ccded..ced6c86cd2fd700386bc0ec6305511b90d25933a 100644
--- a/src/vkcv/CommandStreamManager.cpp
+++ b/src/vkcv/CommandStreamManager.cpp
@@ -4,105 +4,76 @@
 #include "vkcv/Logger.hpp"
 
 namespace vkcv {
-	CommandStreamManager::CommandStreamManager() noexcept : m_core(nullptr){}
-
-	CommandStreamManager::~CommandStreamManager() noexcept {
-		for (const auto& stream : m_commandStreams) {
-			if (stream.cmdBuffer && stream.cmdBuffer) {
-				m_core->getContext().getDevice().freeCommandBuffers(stream.cmdPool, stream.cmdBuffer);
-			}
-		}
+	
+	uint64_t CommandStreamManager::getIdFrom(const CommandStreamHandle &handle) const {
+		return handle.getId();
 	}
-
-	void CommandStreamManager::init(Core* core) {
-		if (!core) {
-			vkcv_log(LogLevel::ERROR, "Requires valid core pointer");
+	
+	CommandStreamHandle CommandStreamManager::createById(uint64_t id, const HandleDestroyFunction &destroy) {
+		return CommandStreamHandle(id, destroy);
+	}
+	
+	void CommandStreamManager::destroyById(uint64_t id) {
+		auto& stream = getById(id);
+		
+		if (stream.cmdBuffer) {
+			getCore().getContext().getDevice().freeCommandBuffers(stream.cmdPool, stream.cmdBuffer);
+			stream.cmdBuffer = nullptr;
+			stream.callbacks.clear();
 		}
-		m_core = core;
 	}
+	
+	CommandStreamManager::CommandStreamManager() noexcept :
+		HandleManager<CommandStreamEntry, CommandStreamHandle>() {}
 
-	CommandStreamHandle CommandStreamManager::createCommandStream(
-		const vk::Queue &queue,
-		vk::CommandPool cmdPool) {
-
-		const vk::CommandBuffer cmdBuffer = allocateCommandBuffer(m_core->getContext().getDevice(), cmdPool);
-
-		CommandStream stream(cmdBuffer, queue, cmdPool);
-		beginCommandBuffer(stream.cmdBuffer, vk::CommandBufferUsageFlagBits::eOneTimeSubmit);
+	CommandStreamManager::~CommandStreamManager() noexcept {
+		clear();
+	}
 
-		// find unused stream
-		int unusedStreamIndex = -1;
-		for (size_t i = 0; i < m_commandStreams.size(); i++) {
-			if (m_commandStreams[i].cmdBuffer) {
-				// still in use
-			} else {
-				unusedStreamIndex = i;
-				break;
+	CommandStreamHandle CommandStreamManager::createCommandStream(const vk::Queue &queue,
+																  vk::CommandPool cmdPool) {
+		const vk::CommandBuffer cmdBuffer = allocateCommandBuffer(getCore().getContext().getDevice(), cmdPool);
+		beginCommandBuffer(cmdBuffer, vk::CommandBufferUsageFlagBits::eOneTimeSubmit);
+		
+		for (uint64_t id = 0; id < getCount(); id++) {
+			auto& stream = getById(id);
+			
+			if (!(stream.cmdBuffer)) {
+				stream.cmdBuffer = cmdBuffer;
+				stream.cmdPool = cmdPool;
+				stream.queue = queue;
+				
+				return createById(id, [&](uint64_t id) { destroyById(id); });
 			}
 		}
-
-        const bool foundUnusedStream = unusedStreamIndex >= 0;
-        if (foundUnusedStream) {
-            m_commandStreams[unusedStreamIndex] = stream;
-            return CommandStreamHandle(unusedStreamIndex);
-        }
-
-        CommandStreamHandle handle(m_commandStreams.size());
-        m_commandStreams.push_back(stream);
-        return handle;
+		
+        return add({ cmdBuffer, cmdPool, queue, {} });
     }
 
-	void CommandStreamManager::recordCommandsToStream(
-		const CommandStreamHandle   &handle,
-		const RecordCommandFunction &record) {
-
-		const size_t id = handle.getId();
-		if (id >= m_commandStreams.size()) {
-			vkcv_log(LogLevel::ERROR, "Requires valid handle");
-			return;
-		}
-
-		CommandStream& stream = m_commandStreams[id];
+	void CommandStreamManager::recordCommandsToStream(const CommandStreamHandle &handle,
+													  const RecordCommandFunction &record) {
+		auto& stream = (*this)[handle];
 		record(stream.cmdBuffer);
 	}
 
-	void CommandStreamManager::addFinishCallbackToStream(
-		const CommandStreamHandle   &handle,
-		const FinishCommandFunction &finish) {
-
-		const size_t id = handle.getId();
-		if (id >= m_commandStreams.size()) {
-			vkcv_log(LogLevel::ERROR, "Requires valid handle");
-			return;
-		}
-
-		CommandStream& stream = m_commandStreams[id];
+	void CommandStreamManager::addFinishCallbackToStream(const CommandStreamHandle &handle,
+														 const FinishCommandFunction &finish) {
+		auto& stream = (*this)[handle];
 		stream.callbacks.push_back(finish);
 	}
 
-	void CommandStreamManager::submitCommandStreamSynchronous(
-		const CommandStreamHandle   &handle,
-		std::vector<vk::Semaphore>  &waitSemaphores,
-		std::vector<vk::Semaphore>  &signalSemaphores) {
-
-		const size_t id = handle.getId();
-		if (id >= m_commandStreams.size()) {
-			vkcv_log(LogLevel::ERROR, "Requires valid handle");
-			return;
-		}
-		CommandStream& stream = m_commandStreams[id];
+	void CommandStreamManager::submitCommandStreamSynchronous(const CommandStreamHandle &handle,
+															  std::vector<vk::Semaphore> &waitSemaphores,
+															  std::vector<vk::Semaphore> &signalSemaphores) {
+		auto& stream = (*this)[handle];
 		stream.cmdBuffer.end();
 
-		const auto device = m_core->getContext().getDevice();
+		const auto device = getCore().getContext().getDevice();
 		const vk::Fence waitFence = createFence(device);
 		submitCommandBufferToQueue(stream.queue, stream.cmdBuffer, waitFence, waitSemaphores, signalSemaphores);
 		waitForFence(device, waitFence);
 		device.destroyFence(waitFence);
-
-		device.freeCommandBuffers(stream.cmdPool, stream.cmdBuffer);
-		stream.cmdBuffer    = nullptr;
-		stream.cmdPool      = nullptr;
-		stream.queue        = nullptr;
+		stream.queue = nullptr;
 
 		for (const auto& finishCallback : stream.callbacks) {
 			finishCallback();
@@ -110,11 +81,8 @@ namespace vkcv {
 	}
 
 	vk::CommandBuffer CommandStreamManager::getStreamCommandBuffer(const CommandStreamHandle &handle) {
-		const size_t id = handle.getId();
-		if (id >= m_commandStreams.size()) {
-			vkcv_log(LogLevel::ERROR, "Requires valid handle");
-			return nullptr;
-		}
-		return m_commandStreams[id].cmdBuffer;
+		auto& stream = (*this)[handle];
+		return stream.cmdBuffer;
 	}
+	
 }
\ No newline at end of file
diff --git a/src/vkcv/CommandStreamManager.hpp b/src/vkcv/CommandStreamManager.hpp
index 9a8eb3d02143c20532275d5a61ca3cc68aa03778..044cb6bc0fc8758faa4973451b7cf9785c5e61ff 100644
--- a/src/vkcv/CommandStreamManager.hpp
+++ b/src/vkcv/CommandStreamManager.hpp
@@ -1,49 +1,44 @@
 #pragma once
+
 #include <vulkan/vulkan.hpp>
 #include <vector>
+
 #include "vkcv/Event.hpp"
-#include "vkcv/Handles.hpp"
 #include "vkcv/CommandRecordingFunctionTypes.hpp"
 
+#include "HandleManager.hpp"
+
 namespace vkcv {
 	
-	class Core;
+	/**
+	 * @brief Represents one command stream, into which commands can be recorded into.
+	 * Consists of a command buffer, the command buffer's command pool and a queue, as well as some callbacks.
+	 */
+	struct CommandStreamEntry {
+		vk::CommandBuffer                   cmdBuffer;
+		vk::CommandPool                     cmdPool;
+		vk::Queue                           queue;
+		std::vector<FinishCommandFunction>  callbacks;
+	};
 
 	/**
 	 * @brief Responsible for creation, deletion, callbacks and recording of command streams
 	*/
-	class CommandStreamManager
-	{
+	class CommandStreamManager : public HandleManager<CommandStreamEntry, CommandStreamHandle> {
 		friend class Core;
 	private:
-		/**
-		 * @brief Represents one command stream, into which commands can be recorded into.
-		 * Consists of a command buffer, the command buffer's command pool and a queue, as well as some callbacks.
-		*/
-		struct CommandStream {
-			inline CommandStream(vk::CommandBuffer cmdBuffer, vk::Queue queue, vk::CommandPool cmdPool) 
-				: cmdBuffer(cmdBuffer), cmdPool(cmdPool), queue(queue) {};
-			vk::CommandBuffer                   cmdBuffer;
-			vk::CommandPool                     cmdPool;
-			vk::Queue                           queue;
-			std::vector<FinishCommandFunction>  callbacks;
-		};
-
-		Core* m_core;
-		std::vector<CommandStream> m_commandStreams;
-
-		CommandStreamManager() noexcept;
-
-		void init(Core* core);
+		[[nodiscard]]
+		uint64_t getIdFrom(const CommandStreamHandle& handle) const override;
+		
+		[[nodiscard]]
+		CommandStreamHandle createById(uint64_t id, const HandleDestroyFunction& destroy) override;
+		
+		void destroyById(uint64_t id) override;
 
 	public:
-		~CommandStreamManager() noexcept;
-
-		CommandStreamManager(CommandStreamManager&& other) = delete;
-		CommandStreamManager(const CommandStreamManager& other) = delete;
-
-		CommandStreamManager& operator=(CommandStreamManager&& other) = delete;
-		CommandStreamManager& operator=(const CommandStreamManager& other) = delete;
+		CommandStreamManager() noexcept;
+		
+		~CommandStreamManager() noexcept override;
 
 		/**
 		 * @brief Creates a new command stream
@@ -52,9 +47,8 @@ namespace vkcv {
 		 * @param cmdPool Command pool the command buffer will be allocated from
 		 * @return Handle that represents the #CommandStream
 		*/
-		CommandStreamHandle createCommandStream(
-			const vk::Queue &queue,
-			vk::CommandPool cmdPool);
+		CommandStreamHandle createCommandStream(const vk::Queue &queue,
+												vk::CommandPool cmdPool);
 
 		/**
 		 * @brief Record vulkan commands to a #CommandStream, using a record function
@@ -62,7 +56,8 @@ namespace vkcv {
 		 * @param handle Command stream handle
 		 * @param record Function that records the vulkan commands
 		*/
-		void recordCommandsToStream(const CommandStreamHandle &handle, const RecordCommandFunction &record);
+		void recordCommandsToStream(const CommandStreamHandle &handle,
+									const RecordCommandFunction &record);
 
 		/**
 		 * @brief Add a callback to a #CommandStream that is called 
@@ -71,7 +66,8 @@ namespace vkcv {
 		 * @param handle Command stream handle
 		 * @param finish Callback that is called when a command stream submission is finished
 		*/
-		void addFinishCallbackToStream(const CommandStreamHandle &handle, const FinishCommandFunction &finish);
+		void addFinishCallbackToStream(const CommandStreamHandle &handle,
+									   const FinishCommandFunction &finish);
 
 		/**
 		 * @brief Submits a #CommandStream to it's queue and returns after execution is finished
@@ -80,10 +76,9 @@ namespace vkcv {
 		 * @param waitSemaphores Semaphores that are waited upon before executing the recorded commands
 		 * @param signalSemaphores Semaphores that are signaled when execution of the recorded commands is finished
 		*/
-		void submitCommandStreamSynchronous(
-			const CommandStreamHandle   &handle,
-			std::vector<vk::Semaphore>  &waitSemaphores,
-			std::vector<vk::Semaphore>  &signalSemaphores);
+		void submitCommandStreamSynchronous(const CommandStreamHandle   &handle,
+											std::vector<vk::Semaphore>  &waitSemaphores,
+											std::vector<vk::Semaphore>  &signalSemaphores);
 
 		/**
 		 * @brief Returns the underlying vulkan handle of a #CommandStream to be used for manual command recording
@@ -92,6 +87,7 @@ namespace vkcv {
 		 * @return Vulkan handle of the #CommandStream
 		*/
 		vk::CommandBuffer getStreamCommandBuffer(const CommandStreamHandle &handle);
+		
 	};
 
 }
\ No newline at end of file
diff --git a/src/vkcv/Core.cpp b/src/vkcv/Core.cpp
index 821ae5e334e7f2be4e4c9d156d2a774a232e4718..d54e5c8594ac710f9919ac0d5cce1d66e1c4e2c3 100644
--- a/src/vkcv/Core.cpp
+++ b/src/vkcv/Core.cpp
@@ -57,9 +57,9 @@ namespace vkcv
 			m_DescriptorSetLayoutManager(std::make_unique<DescriptorSetLayoutManager>()),
 			m_DescriptorSetManager(std::make_unique<DescriptorSetManager>()),
 			m_BufferManager(std::make_unique<BufferManager>()),
-			m_SamplerManager(std::unique_ptr<SamplerManager>(new SamplerManager(m_Context.m_Device))),
+			m_SamplerManager(std::make_unique<SamplerManager>()),
 			m_ImageManager(std::make_unique<ImageManager>()),
-			m_CommandStreamManager{std::unique_ptr<CommandStreamManager>(new CommandStreamManager)},
+			m_CommandStreamManager{std::make_unique<CommandStreamManager>()},
 			m_WindowManager(std::make_unique<WindowManager>()),
 			m_SwapchainManager(std::make_unique<SwapchainManager>()),
 			m_CommandResources(commandResources),
@@ -72,7 +72,8 @@ namespace vkcv
 		m_DescriptorSetLayoutManager->init(*this);
 		m_DescriptorSetManager->init(*this, *m_DescriptorSetLayoutManager);
 		m_BufferManager->init(*this);
-		m_CommandStreamManager->init(this);
+		m_SamplerManager->init(*this);
+		m_CommandStreamManager->init(*this);
 		m_SwapchainManager->m_context = &m_Context;
 		m_ImageManager->init(*this, *m_BufferManager);
 		m_downsampler = std::unique_ptr<Downsampler>(new BlitDownsampler(*this, *m_ImageManager));
diff --git a/src/vkcv/SamplerManager.cpp b/src/vkcv/SamplerManager.cpp
index 9a80635744e5a3dd0b6bd8db476cec841b1c317d..49accf6892560c1c8d3cd856cd1dd207bb214399 100644
--- a/src/vkcv/SamplerManager.cpp
+++ b/src/vkcv/SamplerManager.cpp
@@ -4,16 +4,29 @@
 
 namespace vkcv {
 	
-	SamplerManager::SamplerManager(const vk::Device& device) noexcept :
-		m_device(device), m_samplers()
-	{}
+	uint64_t SamplerManager::getIdFrom(const SamplerHandle &handle) const {
+		return handle.getId();
+	}
+	
+	SamplerHandle SamplerManager::createById(uint64_t id, const HandleDestroyFunction &destroy) {
+		return SamplerHandle(id, destroy);
+	}
 	
-	SamplerManager::~SamplerManager() {
-		for (uint64_t id = 0; id < m_samplers.size(); id++) {
-			destroySamplerById(id);
+	void SamplerManager::destroyById(uint64_t id) {
+		auto& sampler = getById(id);
+		
+		if (sampler) {
+			getCore().getContext().getDevice().destroySampler(sampler);
+			sampler = nullptr;
 		}
 	}
 	
+	SamplerManager::SamplerManager() noexcept : HandleManager<vk::Sampler, SamplerHandle>() {}
+	
+	SamplerManager::~SamplerManager() noexcept {
+		clear();
+	}
+	
 	SamplerHandle SamplerManager::createSampler(SamplerFilterType magFilter,
 												SamplerFilterType minFilter,
 												SamplerMipmapMode mipmapMode,
@@ -121,34 +134,15 @@ namespace vkcv {
 				false
 		);
 		
-		const vk::Sampler sampler = m_device.createSampler(samplerCreateInfo);
+		const vk::Sampler sampler = getCore().getContext().getDevice().createSampler(
+				samplerCreateInfo
+		);
 		
-		const uint64_t id = m_samplers.size();
-		m_samplers.push_back(sampler);
-		return SamplerHandle(id, [&](uint64_t id) { destroySamplerById(id); });
+		return add(sampler);
 	}
 	
 	vk::Sampler SamplerManager::getVulkanSampler(const SamplerHandle &handle) const {
-		const uint64_t id = handle.getId();
-		
-		if (id >= m_samplers.size()) {
-			return nullptr;
-		}
-		
-		return m_samplers[id];
-	}
-	
-	void SamplerManager::destroySamplerById(uint64_t id) {
-		if (id >= m_samplers.size()) {
-			return;
-		}
-		
-		auto& sampler = m_samplers[id];
-		
-		if (sampler) {
-			m_device.destroySampler(sampler);
-			sampler = nullptr;
-		}
+		return (*this)[handle];
 	}
 
 }
diff --git a/src/vkcv/SamplerManager.hpp b/src/vkcv/SamplerManager.hpp
index 24d89f0d6af151ff009a2f2bb64953d48a9e7213..3852d3be559b679710a64176ebe96c271ca92298 100644
--- a/src/vkcv/SamplerManager.hpp
+++ b/src/vkcv/SamplerManager.hpp
@@ -3,34 +3,30 @@
 #include <vector>
 #include <vulkan/vulkan.hpp>
 
-#include "vkcv/Handles.hpp"
+#include "HandleManager.hpp"
+
 #include "vkcv/Sampler.hpp"
 
 namespace vkcv {
 	
-	class Core;
-	
 	/**
 	 * @brief Class to manage the creation and destruction of samplers.
 	 */
-	class SamplerManager {
+	class SamplerManager : public HandleManager<vk::Sampler, SamplerHandle> {
 		friend class Core;
 	private:
-		vk::Device m_device;
-		std::vector<vk::Sampler> m_samplers;
+		[[nodiscard]]
+		uint64_t getIdFrom(const SamplerHandle& handle) const override;
 		
-		explicit SamplerManager(const vk::Device& device) noexcept;
+		[[nodiscard]]
+		SamplerHandle createById(uint64_t id, const HandleDestroyFunction& destroy) override;
 		
-		void destroySamplerById(uint64_t id);
+		void destroyById(uint64_t id) override;
 		
 	public:
-		~SamplerManager();
-		
-		SamplerManager(const SamplerManager& other) = delete;
-		SamplerManager(SamplerManager&& other) = delete;
+		SamplerManager() noexcept;
 		
-		SamplerManager& operator=(const SamplerManager& other) = delete;
-		SamplerManager& operator=(SamplerManager&& other) = delete;
+		~SamplerManager() noexcept override;
 		
 		SamplerHandle createSampler(SamplerFilterType magFilter,
 									SamplerFilterType minFilter,