diff --git a/src/collections/mod.rs b/src/collections/mod.rs
index 0802f59..fa842ed 100644
--- a/src/collections/mod.rs
+++ b/src/collections/mod.rs
@@ -1,6 +1,3 @@
-/// Collections types for Zenyx
-///
-/// - [`SparseSet`]
 mod sparse_set;
 
 pub use sparse_set::SparseSet;
diff --git a/src/collections/sparse_set.rs b/src/collections/sparse_set.rs
index a23117c..75b04ea 100644
--- a/src/collections/sparse_set.rs
+++ b/src/collections/sparse_set.rs
@@ -10,51 +10,16 @@ use bytemuck::Contiguous;
 const SPARSE_PAGESIZE: usize = (1 << 10) * 4;
 type SparsePage<A> = Option<(Box<[Option<NonZeroUsize>; SPARSE_PAGESIZE], A>, usize)>;
 
-/// A sparse set for fast lookup of large indices.
-///
-/// The sparse allocator is mainly used for bulk allocations in the system's page size
-/// for the lookup array. It will also be used for the array of pointers into those
-/// bulk allocations. Additionally it will be used for the reverse map that generates keys
-/// from the value in the internal packed array.
-///
-/// The packed allocator will exclusively be used to store the values of type `T`.
-///
-/// All operations on this datastructure, meaning insertion, lookup, and deletion, are `O(1)`.
-///
-/// This data structure does not in any way guarantee ordering of the values on
-/// its own.
-#[derive(Hash)]
 pub struct SparseSet<T, PackedAlloc = Global, SparseAlloc = Global>
 where
     PackedAlloc: Allocator,
     SparseAlloc: Allocator,
 {
-    /// The paginated array of keys. The value at the key is an index into the dense array minus 
-    /// one where the value corresponding to that key is stored.
     sparse: Vec<SparsePage<SparseAlloc>, SparseAlloc>,
-    /// The dense array where the values corresponding to the keys are stored.
     dense: Vec<T, PackedAlloc>,
-    /// The reverse map to get the index in the sparse array from the index in the dense array.
     dense_to_id: Vec<usize, SparseAlloc>,
 }
 
-impl<T> SparseSet<T> {
-    /// Creates a new [`SparseSet`] with the global allocator.
-    pub const fn new() -> Self {
-        Self {
-            sparse: Vec::new(),
-            dense: Vec::new(),
-            dense_to_id: Vec::new(),
-        }
-    }
-}
-
-impl<T> Default for SparseSet<T> {
-    fn default() -> Self {
-        Self::new()
-    }
-}
-
 impl<T, PackedAlloc, SparseAlloc> core::fmt::Debug for SparseSet<T, PackedAlloc, SparseAlloc>
 where
     T: core::fmt::Debug,
@@ -68,26 +33,21 @@ where
     }
 }
 
+impl<T> SparseSet<T> {
+    pub const fn new() -> Self {
+        Self {
+            sparse: Vec::new(),
+            dense: Vec::new(),
+            dense_to_id: Vec::new(),
+        }
+    }
+}
+
 impl<T, PackedAlloc, SparseAlloc> SparseSet<T, PackedAlloc, SparseAlloc>
 where
     PackedAlloc: Allocator,
     SparseAlloc: Allocator + Clone,
 {
-    /// Inserts an element into the sparse set with the key `id`. This will 
-    /// return the previous value if it already exists.
-    ///
-    /// ```
-    /// use zenyx::collections::SparseSet;
-    ///
-    /// let mut sparse_set: SparseSet<u32> = SparseSet::new();
-    ///
-    /// sparse_set.insert(10, 123);
-    /// assert_eq!(sparse_set.get(10), Some(&123));
-    ///
-    /// let prev = sparse_set.insert(10, 9);
-    /// assert_eq!(prev, Some(123));
-    /// assert_eq!(sparse_set.get(10), Some(&9));
-    /// ```
     pub fn insert(&mut self, id: usize, value: T) -> Option<T> {
         match self.get_dense_idx(id) {
             Some(idx) => {
@@ -105,39 +65,15 @@ where
         }
     }
 
-    /// Gets the value with the key `id`.
-    ///
-    /// ```
-    /// use zenyx::collections::SparseSet;
-    ///
-    /// let mut sparse_set: SparseSet<u32> = SparseSet::new();
-    ///
-    /// sparse_set.insert(10, 123);
-    /// assert_eq!(sparse_set.get(10), Some(&123));
-    /// ```
     pub fn get(&self, id: usize) -> Option<&T> {
         self.dense.get(self.get_dense_idx(id)?)
     }
 
-    /// Gets the value with the key `id` mutably.
-    ///
-    /// ```
-    /// use zenyx::collections::SparseSet;
-    ///
-    /// let mut sparse_set: SparseSet<u32> = SparseSet::new();
-    ///
-    /// sparse_set.insert(10, 123);
-    /// let value = sparse_set.get_mut(10).unwrap();
-    /// *value = 0;
-    /// assert_eq!(sparse_set.get(10), Some(&0));
-    /// ```
     pub fn get_mut(&mut self, id: usize) -> Option<&mut T> {
         let idx = self.get_dense_idx(id)?;
         self.dense.get_mut(idx)
     }
 
-    /// Sets the dense index of an `key` to `idx`. This will remove said index 
-    /// if it is [`None`].
     fn set_dense_idx(&mut self, id: usize, idx: Option<usize>) {
         let page = id / SPARSE_PAGESIZE;
         let sparse_index = id % SPARSE_PAGESIZE;
@@ -161,15 +97,6 @@ where
         }
     }
 
-    /// Gets the index in the dense array for a key `id`.
-    ///
-    /// ```
-    /// use zenyx::collections::SparseSet;
-    ///
-    /// let mut sparse_set: SparseSet<u32> = SparseSet::new();
-    /// sparse_set.insert(10, 123);
-    /// assert_eq!(sparse_set.values()[sparse_set.get_dense_idx(10).unwrap()], 123);
-    /// ```
     pub fn get_dense_idx(&self, id: usize) -> Option<usize> {
         let page = id / SPARSE_PAGESIZE;
         let sparse_index = id % SPARSE_PAGESIZE;
@@ -177,8 +104,6 @@ where
         page.0[sparse_index].map(|idx| idx.into_integer() - 1)
     }
 
-    /// This reduces the usage count for a page in the sparse array, deallocating 
-    /// it if it is not used anymore.
     fn reduce_page_usage_count(&mut self, id: usize) {
         let page = id / SPARSE_PAGESIZE;
         let Some(usage) = &mut self.sparse[page] else {
@@ -191,7 +116,6 @@ where
         }
     }
 
-    /// Increase the page usage count for a page in the sparse array.
     fn increase_page_usage_count(&mut self, id: usize) {
         let page = id / SPARSE_PAGESIZE;
         if page >= self.sparse.len() {
@@ -203,18 +127,6 @@ where
         usage.1 += 1;
     }
 
-    /// Removes the value with the key `id` from the sparse set, returning the
-    /// value if it existed.
-    ///
-    /// ```
-    /// use zenyx::collections::SparseSet;
-    ///
-    /// let mut sparse_set: SparseSet<u32> = SparseSet::new();
-    ///
-    /// sparse_set.insert(10, 123);
-    /// assert_eq!(sparse_set.remove(10), Some(123));
-    /// assert_eq!(sparse_set.remove(10), None);
-    /// ```
     pub fn remove(&mut self, id: usize) -> Option<T> {
         let index = self.get_dense_idx(id)?;
         if self.dense.is_empty() {
@@ -230,126 +142,26 @@ where
         Some(previous)
     }
 
-    /// Returns if there are values in this sparse set.
-    /// ```
-    /// use zenyx::collections::SparseSet;
-    ///
-    /// let mut sparse_set: SparseSet<u32> = SparseSet::new();
-    /// assert!(sparse_set.is_empty());
-    /// sparse_set.insert(10, 123);
-    /// assert!(!sparse_set.is_empty());
-    /// ```
     pub fn is_empty(&self) -> bool {
         self.len() == 0
     }
 
-    /// Returns the number of values in this sparse set.
-    /// 
-    /// ```
-    /// use zenyx::collections::SparseSet;
-    ///
-    /// let mut sparse_set: SparseSet<u32> = SparseSet::new();
-    /// sparse_set.insert(10, 123);
-    /// sparse_set.insert(10, 9);
-    /// sparse_set.insert(11, 10);
-    /// assert_eq!(sparse_set.len(), 2);
-    /// ```
     pub fn len(&self) -> usize {
         self.dense.len()
     }
 
-    /// Checks if the sparse set contains a value with key `id`.
-    ///
-    /// ```
-    /// use zenyx::collections::SparseSet;
-    ///
-    /// let mut sparse_set: SparseSet<u32> = SparseSet::new();
-    /// assert!(!sparse_set.contains(10));
-    /// sparse_set.insert(10, 123);
-    /// assert!(sparse_set.contains(10));
-    /// ```
     pub fn contains(&self, id: usize) -> bool {
         self.get_dense_idx(id).is_some()
     }
 
-    /// Gets the keys of all values in the sparse set. This method does not provide
-    /// any ordering guarantees other than the keys contained corresponding to 
-    /// the values with the same index returned by [`Self::values`].
-    ///
-    /// ```
-    /// use zenyx::collections::SparseSet;
-    ///
-    /// let mut sparse_set: SparseSet<u32> = SparseSet::new();
-    /// assert!(sparse_set.keys().is_empty());
-    /// sparse_set.insert(10, 10);
-    /// sparse_set.insert(9, 10);
-    /// sparse_set.insert(11, 10);
-    ///
-    /// assert_eq!(sparse_set.keys(), &[10, 9, 11]);
-    /// sparse_set.remove(10);
-    /// assert_eq!(sparse_set.keys(), &[11, 9]);
-    /// ```
     pub fn keys(&self) -> &[usize] {
         &self.dense_to_id
     }
 
-    /// Gets all values in the sparse set, the corresponding `key` is at the same 
-    /// position in the slice returned by [`Self::keys`].
-    ///
-    /// Otherwise there are no ordering guarantees.
-    /// 
-    /// ```
-    /// use zenyx::collections::SparseSet;
-    ///
-    /// let mut sparse_set: SparseSet<u32> = SparseSet::new();
-    /// assert!(sparse_set.values().is_empty());
-    /// sparse_set.insert(10, 10);
-    /// sparse_set.insert(9, 9);
-    /// sparse_set.insert(11, 11);
-    ///
-    /// assert_eq!(sparse_set.values(), &[10, 9, 11]);
-    /// sparse_set.remove(10);
-    /// assert_eq!(sparse_set.values(), &[11, 9]);
-    /// ```
     pub fn values(&self) -> &[T] {
         &self.dense
     }
 
-    /// Mutable version of [`Self::keys`].
-    ///
-    /// ```
-    /// use zenyx::collections::SparseSet;
-    ///
-    /// let mut sparse_set: SparseSet<u32> = SparseSet::new();
-    /// assert!(sparse_set.values().is_empty());
-    /// sparse_set.insert(10, 10);
-    /// sparse_set.insert(9, 9);
-    /// sparse_set.insert(11, 11);
-    ///
-    /// let dense_of_9 = sparse_set.get_dense_idx(9).unwrap();
-    /// let dense_of_10 = sparse_set.get_dense_idx(10).unwrap();
-    ///
-    /// let values = sparse_set.values_mut();
-    /// values[dense_of_10] = 9;
-    /// values[dense_of_9] = 10;
-    ///
-    /// assert_eq!(sparse_set.get(10), Some(&9));
-    /// assert_eq!(sparse_set.get(9), Some(&10));
-    /// ```
-    pub fn values_mut(&mut self) -> &mut [T] {
-        &mut self.dense
-    }
-
-    /// Creates a new [`SparseSet`] with the values allocated by the `packed_alloc`
-    /// and everything else, as described in the top level documentation for [`SparseSet`]
-    /// in the `sparse_alloc`.
-    ///
-    /// ```
-    /// use zenyx::collections::SparseSet;
-    /// use allocator_api2::alloc::Global;
-    ///
-    /// let sparse_set = SparseSet::<u32>::new_in(Global, Global);
-    /// ```
     pub fn new_in(packed_alloc: PackedAlloc, sparse_alloc: SparseAlloc) -> Self {
         Self {
             dense: Vec::new_in(packed_alloc),
@@ -363,16 +175,6 @@ impl<T, PackedAlloc> SparseSet<T, PackedAlloc>
 where
     PackedAlloc: Allocator,
 {
-    /// Creates a new [`SparseSet`] with the values allocated by the `packed_alloc`
-    /// Everything else, as described in the top level documentation for [`SparseSet`]
-    /// is allocated using the global allocator.
-    ///
-    /// ```
-    /// use zenyx::collections::SparseSet;
-    /// use allocator_api2::alloc::Global;
-    ///
-    /// let sparse_set = SparseSet::<u32>::new_in_packed(Global);
-    /// ```
     pub const fn new_in_packed(packed_alloc: PackedAlloc) -> Self {
         Self {
             sparse: Vec::new(),
diff --git a/src/ecs/component.rs b/src/ecs/component.rs
new file mode 100644
index 0000000..0fce562
--- /dev/null
+++ b/src/ecs/component.rs
@@ -0,0 +1,73 @@
+use core::{any::TypeId, marker::PhantomData, num::NonZeroU64, sync::atomic::AtomicU64};
+use std::{collections::BTreeMap, sync::RwLock};
+
+use allocator_api2::alloc::{Allocator, Global};
+use bytemuck::Contiguous;
+
+static COMPONENT_ID_CREATOR: AtomicU64 = AtomicU64::new(1);
+static COMPONENT_IDS: RwLock<BTreeMap<TypeId, ComponentId>> = RwLock::new(BTreeMap::new());
+
+pub fn create_component_id<T>() -> ComponentId
+where
+    T: 'static + Sized,
+{
+    let type_id = core::any::TypeId::of::<T>();
+    {
+        // unwrap-justification: this only errors if RwLock is poisoned
+        let component_read = COMPONENT_IDS.read().unwrap();
+        if let Some(id) = component_read.get(&type_id) {
+            return *id;
+        }
+    }
+
+    let new_id = ComponentId(
+        NonZeroU64::new(COMPONENT_ID_CREATOR.fetch_add(1, core::sync::atomic::Ordering::Relaxed))
+            .unwrap(),
+    );
+    {
+        // unwrap-justification: see above
+        let mut component_write = COMPONENT_IDS.write().unwrap();
+        component_write.insert(type_id, new_id);
+    }
+    new_id
+}
+
+pub trait ComponentAllocator: Allocator {
+    fn new() -> Self;
+}
+
+impl ComponentAllocator for Global {
+    fn new() -> Self {
+        Self
+    }
+}
+
+pub trait Component: core::fmt::Debug + Send + Sized + 'static {
+    type Allocator: ComponentAllocator;
+
+    fn id() -> ComponentId {
+        static COMPONENT_ID: AtomicU64 = AtomicU64::new(0);
+        let mut current_id = COMPONENT_ID.load(core::sync::atomic::Ordering::Relaxed);
+        if current_id == 0 {
+            current_id = create_component_id::<Self>().to_int();
+            COMPONENT_ID.store(current_id, core::sync::atomic::Ordering::Relaxed);
+        }
+        ComponentId(NonZeroU64::new(current_id).unwrap())
+    }
+}
+
+#[repr(transparent)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct ComponentId(NonZeroU64);
+
+impl ComponentId {
+    pub fn to_int(self) -> u64 {
+        self.0.into_integer()
+    }
+}
+
+impl From<ComponentId> for u64 {
+    fn from(value: ComponentId) -> Self {
+        value.to_int()
+    }
+}
diff --git a/src/ecs/entity.rs b/src/ecs/entity.rs
new file mode 100644
index 0000000..ce67bcd
--- /dev/null
+++ b/src/ecs/entity.rs
@@ -0,0 +1,26 @@
+use core::sync::atomic::{AtomicU64, Ordering};
+
+#[repr(transparent)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct Entity(u64);
+
+fn create_entity() -> Entity {
+    static ENTITY_ID: AtomicU64 = AtomicU64::new(0);
+    Entity(ENTITY_ID.fetch_add(1, Ordering::Relaxed))
+}
+
+impl Entity {
+    pub fn new() -> Self {
+        create_entity()
+    }
+
+    pub fn to_int(self) -> u64 {
+        self.0
+    }
+}
+
+impl From<Entity> for u64 {
+    fn from(value: Entity) -> Self {
+        value.to_int()
+    }
+}
diff --git a/src/ecs/mod.rs b/src/ecs/mod.rs
new file mode 100644
index 0000000..a3e2248
--- /dev/null
+++ b/src/ecs/mod.rs
@@ -0,0 +1,19 @@
+mod component;
+pub use component::{Component, ComponentAllocator, ComponentId};
+mod entity;
+pub use entity::Entity;
+mod storage;
+
+use self::storage::ComponentSet;
+
+pub type ECS = EntityComponentSystem;
+
+pub struct EntityComponentSystem {
+    components: ComponentSet,
+}
+
+impl EntityComponentSystem {
+    fn spawn(&mut self) -> Entity {
+        Entity::new()
+    }
+}
diff --git a/src/ecs/storage.rs b/src/ecs/storage.rs
new file mode 100644
index 0000000..19d9d8e
--- /dev/null
+++ b/src/ecs/storage.rs
@@ -0,0 +1,75 @@
+use allocator_api2::alloc::Allocator;
+
+use crate::collections::SparseSet;
+
+use super::{Component, ComponentAllocator, Entity};
+
+pub trait ComponentStorage: core::fmt::Debug + 'static {}
+
+impl<T, SparseAlloc> ComponentStorage for SparseSet<T, T::Allocator, SparseAlloc>
+where
+    T: Component,
+    SparseAlloc: Allocator + Clone + 'static,
+{
+}
+
+#[derive(Debug)]
+pub struct ComponentSet<A = allocator_api2::alloc::Global> {
+    sets: SparseSet<Box<dyn ComponentStorage>>,
+    cold_alloc: A,
+}
+
+impl ComponentSet {
+    fn new() -> Self {
+        Self {
+            sets: SparseSet::new(),
+            cold_alloc: allocator_api2::alloc::Global,
+        }
+    }
+}
+
+impl<A> ComponentSet<A>
+where
+    A: Allocator + Clone + 'static,
+{
+    fn new_in(alloc: A) -> Self {
+        Self {
+            sets: SparseSet::new(),
+            cold_alloc: alloc,
+        }
+    }
+
+    fn get_component_set<T: Component>(&self) -> Option<&SparseSet<T, T::Allocator, A>> {
+        let set = self.sets.get(T::id().to_int() as usize)?;
+        (set as &dyn core::any::Any).downcast_ref()
+    }
+
+    fn get_component_set_mut<T: Component>(
+        &mut self,
+    ) -> Option<&mut SparseSet<T, T::Allocator, A>> {
+        let set = self.sets.get_mut(T::id().to_int() as usize)?;
+        (set as &mut dyn core::any::Any).downcast_mut()
+    }
+
+    fn insert_component_set<T: Component>(&mut self) -> &mut SparseSet<T, T::Allocator, A> {
+        if self.sets.contains(T::id().to_int() as usize) {
+            self.get_component_set_mut::<T>().unwrap()
+        } else {
+            let set = SparseSet::<T, _, _>::new_in(T::Allocator::new(), self.cold_alloc.clone());
+            self.sets
+                .insert(T::id().to_int() as usize, Box::new(set) as Box<_>);
+            self.get_component_set_mut::<T>().unwrap()
+        }
+    }
+
+    fn add_to_entity<T: Component>(&mut self, entity: Entity, data: T) -> Option<T> {
+        let set = self.insert_component_set::<T>();
+        set.insert(entity.to_int() as usize, data)
+    }
+}
+
+impl Default for ComponentSet {
+    fn default() -> Self {
+        Self::new()
+    }
+}
diff --git a/src/main.rs b/src/main.rs
index 0105e37..6786044 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -19,6 +19,7 @@ use zlog::config::LoggerConfig;
 
 pub mod camera;
 pub mod collections;
+pub mod ecs;
 pub mod model;
 pub mod texture;