diff --git a/aztecs.cabal b/aztecs.cabal
--- a/aztecs.cabal
+++ b/aztecs.cabal
@@ -1,6 +1,6 @@
 cabal-version: 2.4
 name:          aztecs
-version:       0.8.0
+version:       0.9.0
 license:       BSD-3-Clause
 license-file:  LICENSE
 maintainer:    matt@hunzinger.me
@@ -39,23 +39,10 @@
         Aztecs.ECS.Query.Dynamic.Reader.Class
         Aztecs.ECS.Query.Reader
         Aztecs.ECS.Query.Reader.Class
-        Aztecs.ECS.Schedule
-        Aztecs.ECS.Schedule.Access
-        Aztecs.ECS.Schedule.Access.Class
-        Aztecs.ECS.Schedule.Class
-        Aztecs.ECS.Schedule.Dynamic
-        Aztecs.ECS.Schedule.Dynamic.Reader
-        Aztecs.ECS.Schedule.Reader
-        Aztecs.ECS.Schedule.Reader.Class
         Aztecs.ECS.System
         Aztecs.ECS.System.Class
-        Aztecs.ECS.System.Dynamic
         Aztecs.ECS.System.Dynamic.Class
-        Aztecs.ECS.System.Dynamic.Reader
         Aztecs.ECS.System.Dynamic.Reader.Class
-        Aztecs.ECS.System.Queue
-        Aztecs.ECS.System.Queue.Class
-        Aztecs.ECS.System.Reader
         Aztecs.ECS.System.Reader.Class
         Aztecs.ECS.View
         Aztecs.ECS.World
@@ -90,7 +77,8 @@
         deepseq >=1,
         linear >=1,
         mtl >=2,
-        parallel >=3
+        parallel >=3,
+        stm >=2
 
 test-suite aztecs-test
     type:             exitcode-stdio-1.0
diff --git a/bench/Bench.hs b/bench/Bench.hs
--- a/bench/Bench.hs
+++ b/bench/Bench.hs
@@ -5,12 +5,13 @@
 
 import Aztecs.ECS
 import qualified Aztecs.ECS.Query as Q
-import qualified Aztecs.ECS.System as S
-import Aztecs.ECS.World (World (..))
+import Aztecs.ECS.World
 import qualified Aztecs.ECS.World as W
+import Control.Arrow
 import Control.DeepSeq
 import Criterion.Main
-import GHC.Generics (Generic)
+import Data.Functor.Identity
+import GHC.Generics
 
 newtype Position = Position Int deriving (Show, Generic, NFData)
 
@@ -21,21 +22,21 @@
 instance Component Velocity
 
 query :: Query () Position
-query = proc () -> do
-  Velocity v <- Q.fetch -< ()
-  Position p <- Q.fetch -< ()
-  Q.set -< Position $ p + v
+query = Q.fetch >>> Q.adjust (\(Velocity v) (Position p) -> Position $ p + v)
 
-run :: World -> World
-run w = let !(_, es) = Q.map () query $ entities w in w {entities = es}
+queryDo :: Query () Position
+queryDo = proc () -> do
+  Velocity v <- Q.fetch -< ()
+  Q.adjust (\v (Position p) -> Position $ p + v) -< v
 
-runSystem :: World -> IO World
-runSystem w = do
-  (_, _, w') <- runSchedule (system $ S.map query) w ()
-  return w'
+run :: Query () Position -> World -> [Position]
+run q = fst . runIdentity . Q.map () q . entities
 
 main :: IO ()
 main = do
   let go wAcc = snd $ W.spawn (bundle (Position 0) <> bundle (Velocity 1)) wAcc
       !w = foldr (const go) W.empty [0 :: Int .. 10000]
-  defaultMain [bench "iter" $ nf run w, bench "iter system" . nfIO $ runSystem w]
+  defaultMain
+    [ bench "iter" $ nf (run query) w,
+      bench "iter do-notation" $ nf (run queryDo) w
+    ]
diff --git a/src/Aztecs/Asset.hs b/src/Aztecs/Asset.hs
--- a/src/Aztecs/Asset.hs
+++ b/src/Aztecs/Asset.hs
@@ -6,6 +6,14 @@
 {-# LANGUAGE ScopedTypeVariables #-}
 {-# LANGUAGE TypeFamilies #-}
 
+-- |
+-- Module      : Aztecs.Asset
+-- Copyright   : (c) Matt Hunzinger, 2025
+-- License     : BSD-style (see the LICENSE file in the distribution)
+--
+-- Maintainer  : matt@hunzinger.me
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
 module Aztecs.Asset
   ( AssetId (..),
     AssetServer (..),
diff --git a/src/Aztecs/Asset/AssetLoader.hs b/src/Aztecs/Asset/AssetLoader.hs
--- a/src/Aztecs/Asset/AssetLoader.hs
+++ b/src/Aztecs/Asset/AssetLoader.hs
@@ -1,36 +1,58 @@
+{-# LANGUAGE AllowAmbiguousTypes #-}
 {-# LANGUAGE Arrows #-}
 {-# LANGUAGE DerivingStrategies #-}
 {-# LANGUAGE FlexibleInstances #-}
 {-# LANGUAGE GeneralizedNewtypeDeriving #-}
 {-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
 
+-- |
+-- Module      : Aztecs.Asset.AssetLoader
+-- Copyright   : (c) Matt Hunzinger, 2025
+-- License     : BSD-style (see the LICENSE file in the distribution)
+--
+-- Maintainer  : matt@hunzinger.me
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
 module Aztecs.Asset.AssetLoader
-  ( MonadAssetLoader (..),
-    AssetLoader,
+  ( AssetLoader,
     AssetLoaderT (..),
+    MonadAssetLoader (..),
     load,
     loadQuery,
   )
 where
 
 import Aztecs.Asset.AssetLoader.Class
-import Aztecs.Asset.AssetServer (AssetId (..), AssetServer (..), Handle (..))
+import Aztecs.Asset.AssetServer
 import Aztecs.Asset.Class
 import Aztecs.ECS
 import qualified Aztecs.ECS.Query as Q
 import qualified Aztecs.ECS.System as S
-import Control.Arrow (returnA)
-import Control.Concurrent (forkIO)
-import Control.Monad.Identity (Identity)
-import Control.Monad.State.Strict (MonadState (..), StateT, runState)
-import Data.IORef (newIORef, writeIORef)
+import Control.Arrow
+import Control.Concurrent
+import Control.Monad.Identity
+import Control.Monad.State.Strict
+import Data.IORef
 import qualified Data.Map.Strict as Map
 
+-- | @since 9.0
 type AssetLoader a o = AssetLoaderT a Identity o
 
-newtype AssetLoaderT a m o = AssetLoaderT {unAssetLoader :: StateT (AssetServer a) m o}
+-- | Asset loader monad.
+--
+-- @since 9.0
+newtype AssetLoaderT a m o = AssetLoaderT
+  { -- | State of the asset loader.
+    --
+    -- @since 9.0
+    unAssetLoader :: StateT (AssetServer a) m o
+  }
   deriving newtype (Functor, Applicative, Monad)
 
+-- | @since 9.0
 instance (Monad m, Asset a) => MonadAssetLoader a (AssetLoaderT a m) where
   asset path cfg = AssetLoaderT $ do
     server <- get
@@ -48,12 +70,18 @@
         }
     return $ Handle assetId
 
-loadQuery :: (Asset a, ArrowQuery arr) => AssetLoader a o -> arr () o
+-- | Query to load assets.
+--
+-- @since 9.0
+loadQuery :: (Asset a, ArrowQuery m arr) => AssetLoader a o -> arr () o
 loadQuery a = proc () -> do
-  assetServer <- Q.fetch -< ()
-  let (o, assetServer') = runState (unAssetLoader a) assetServer
-  Q.set -< assetServer'
+  server <- Q.fetch -< ()
+  let (o, server') = runState (unAssetLoader a) server
+  Q.set -< server'
   returnA -< o
 
-load :: (ArrowQuery q, ArrowSystem q arr, Asset a) => AssetLoader a o -> arr () o
-load a = S.mapSingle $ loadQuery a
+-- | System to load assets.
+--
+-- @since 9.0
+load :: forall m q s a o. (ArrowQuery m q, MonadSystem q s, Asset a) => AssetLoader a o -> s o
+load a = S.mapSingle @q () $ loadQuery a
diff --git a/src/Aztecs/Asset/AssetLoader/Class.hs b/src/Aztecs/Asset/AssetLoader/Class.hs
--- a/src/Aztecs/Asset/AssetLoader/Class.hs
+++ b/src/Aztecs/Asset/AssetLoader/Class.hs
@@ -1,19 +1,23 @@
-{-# LANGUAGE AllowAmbiguousTypes #-}
-{-# LANGUAGE DerivingStrategies #-}
-{-# LANGUAGE FlexibleContexts #-}
-{-# LANGUAGE FlexibleInstances #-}
 {-# LANGUAGE FunctionalDependencies #-}
-{-# LANGUAGE RankNTypes #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE TypeFamilies #-}
 
-module Aztecs.Asset.AssetLoader.Class
-  ( MonadAssetLoader (..),
-  )
-where
+-- |
+-- Module      : Aztecs.Asset.AssetLoader.Class
+-- Copyright   : (c) Matt Hunzinger, 2025
+-- License     : BSD-style (see the LICENSE file in the distribution)
+--
+-- Maintainer  : matt@hunzinger.me
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
+module Aztecs.Asset.AssetLoader.Class (MonadAssetLoader (..)) where
 
-import Aztecs.Asset.AssetServer (Handle)
+import Aztecs.Asset.AssetServer
 import Aztecs.Asset.Class
 
+-- | Monadic interface for loading assets.
+--
+-- @since 9.0
 class MonadAssetLoader a m | m -> a where
+  -- | Load an asset from a file path with a configuration.
+  --
+  -- @since 9.0
   asset :: FilePath -> AssetConfig a -> m (Handle a)
diff --git a/src/Aztecs/Asset/AssetServer.hs b/src/Aztecs/Asset/AssetServer.hs
--- a/src/Aztecs/Asset/AssetServer.hs
+++ b/src/Aztecs/Asset/AssetServer.hs
@@ -1,22 +1,26 @@
 {-# LANGUAGE AllowAmbiguousTypes #-}
-{-# LANGUAGE Arrows #-}
 {-# LANGUAGE DeriveGeneric #-}
 {-# LANGUAGE DerivingStrategies #-}
-{-# LANGUAGE FlexibleContexts #-}
-{-# LANGUAGE FlexibleInstances #-}
-{-# LANGUAGE RankNTypes #-}
 {-# LANGUAGE ScopedTypeVariables #-}
 {-# LANGUAGE TypeApplications #-}
-{-# LANGUAGE TypeFamilies #-}
 
+-- |
+-- Module      : Aztecs.Asset.AssetServer
+-- Copyright   : (c) Matt Hunzinger, 2025
+-- License     : BSD-style (see the LICENSE file in the distribution)
+--
+-- Maintainer  : matt@hunzinger.me
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
 module Aztecs.Asset.AssetServer
   ( AssetId (..),
+    Handle (..),
     AssetServer (..),
     assetServer,
-    Handle (..),
+    lookupAsset,
     setup,
     loadAssets,
-    lookupAsset,
+    loadAssetServer,
   )
 where
 
@@ -24,31 +28,56 @@
 import qualified Aztecs.ECS.Access as A
 import qualified Aztecs.ECS.Query as Q
 import qualified Aztecs.ECS.System as S
-import Control.Arrow (returnA)
 import Control.DeepSeq
-import Control.Monad.IO.Class (MonadIO (..))
-import Data.Data (Typeable)
-import Data.Foldable (foldrM)
-import Data.IORef (IORef, readIORef)
+import Control.Monad
+import Control.Monad.IO.Class
+import Data.Data
+import Data.Foldable
+import Data.IORef
 import Data.Map.Strict (Map)
 import qualified Data.Map.Strict as Map
-import GHC.Generics (Generic)
+import GHC.Generics
 
-newtype AssetId = AssetId {unAssetId :: Int}
+-- | Unique identifier for an asset.
+--
+-- @since 9.0
+newtype AssetId = AssetId
+  { -- | Unique integer identifier.
+    --
+    -- @since 9.0
+    unAssetId :: Int
+  }
   deriving (Eq, Ord, Show)
 
+-- | Asset server.
+--
+-- @since 9.0
 data AssetServer a = AssetServer
-  { assetServerAssets :: !(Map AssetId a),
+  { -- | Loaded assets.
+    --
+    -- @since 9.0
+    assetServerAssets :: !(Map AssetId a),
+    -- | Assets currently being loaded.
+    --
+    -- @since 9.0
     loadingAssets :: !(Map AssetId (Either (IO (IORef (Maybe a))) (IORef (Maybe a)))),
+    -- | Next unique asset identifier.
+    --
+    -- @since 9.0
     nextAssetId :: !AssetId
   }
   deriving (Generic)
 
+-- | @since 9.0
 instance (Typeable a) => Component (AssetServer a)
 
+-- | @since 9.0
 instance NFData (AssetServer a) where
   rnf = rwhnf
 
+-- | Empty asset server.
+--
+-- @since 9.0
 assetServer :: AssetServer a
 assetServer =
   AssetServer
@@ -57,58 +86,57 @@
       nextAssetId = AssetId 0
     }
 
-newtype Handle a = Handle {handleId :: AssetId}
+-- | Handle to an asset.
+--
+-- @since 9.0
+newtype Handle a = Handle
+  { -- | Asset ID.
+    --
+    -- @since 9.0
+    handleId :: AssetId
+  }
   deriving (Eq, Ord, Show)
 
+-- | @since 9.0
 instance NFData (Handle a) where
   rnf = rwhnf
 
+-- | Lookup an asset by its handle.
+--
+-- @since 9.0
 lookupAsset :: Handle a -> AssetServer a -> Maybe a
 lookupAsset h server = Map.lookup (handleId h) (assetServerAssets server)
 
-loadAssets ::
-  forall a qr rs q s b m arr.
-  ( Typeable a,
-    ArrowQueryReader qr,
-    ArrowReaderSystem qr rs,
-    ArrowReaderSchedule rs arr,
-    ArrowQuery q,
-    ArrowSystem q s,
-    ArrowSchedule s arr,
-    MonadIO m,
-    ArrowAccessSchedule b m arr
-  ) =>
-  arr () ()
-loadAssets = proc () -> do
-  server <- reader $ S.single (Q.fetch @_ @(AssetServer a)) -< ()
-  server' <-
-    access
-      ( \server ->
-          liftIO $
-            foldrM
-              ( \(aId, v) acc -> do
-                  case v of
-                    Right r -> do
-                      maybeSurface <- readIORef r
-                      case maybeSurface of
-                        Just surface ->
-                          return
-                            acc
-                              { assetServerAssets = Map.insert aId surface (assetServerAssets acc),
-                                loadingAssets = Map.delete aId (loadingAssets acc)
-                              }
-                        Nothing -> return acc
-                    Left f -> do
-                      v' <- f
-                      return $ acc {loadingAssets = Map.insert aId (Right v') (loadingAssets server)}
-              )
-              server
-              (Map.toList $ loadingAssets server)
-      )
-      -<
-        server
-  system $ S.mapSingle Q.set -< server'
-  returnA -< ()
+-- | Setup the asset server.
+--
+-- @since 9.0
+setup :: forall m b a. (Typeable a, MonadAccess b m) => m ()
+setup = A.spawn_ . bundle $ assetServer @a
 
-setup :: forall a. (Typeable a) => System () ()
-setup = S.queue . const . A.spawn_ . bundle $ assetServer @a
+-- | Load any pending assets.
+--
+-- @since 9.0
+loadAssets :: forall a q s m. (Typeable a, ArrowQuery m q, MonadSystem q s, MonadIO m) => s ()
+loadAssets = void . S.map @q () $ Q.adjustM (\_ s -> loadAssetServer @m @a s)
+
+-- | Load any pending assets in an `AssetServer`.
+--
+-- @since 9.0
+loadAssetServer :: (MonadIO m) => AssetServer a -> m (AssetServer a)
+loadAssetServer server =
+  let go (aId, v) acc = do
+        case v of
+          Right r -> do
+            maybeSurface <- readIORef r
+            case maybeSurface of
+              Just surface ->
+                return
+                  acc
+                    { assetServerAssets = Map.insert aId surface (assetServerAssets acc),
+                      loadingAssets = Map.delete aId (loadingAssets acc)
+                    }
+              Nothing -> return acc
+          Left f -> do
+            v' <- f
+            return $ acc {loadingAssets = Map.insert aId (Right v') (loadingAssets server)}
+   in liftIO . foldrM go server . Map.toList $ loadingAssets server
diff --git a/src/Aztecs/Asset/Class.hs b/src/Aztecs/Asset/Class.hs
--- a/src/Aztecs/Asset/Class.hs
+++ b/src/Aztecs/Asset/Class.hs
@@ -1,10 +1,27 @@
 {-# LANGUAGE TypeFamilies #-}
 
+-- |
+-- Module      : Aztecs.Asset.Class
+-- Copyright   : (c) Matt Hunzinger, 2025
+-- License     : BSD-style (see the LICENSE file in the distribution)
+--
+-- Maintainer  : matt@hunzinger.me
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
 module Aztecs.Asset.Class (Asset (..)) where
 
-import Data.Data (Typeable)
+import Data.Data
 
+-- | Loadable asset.
+--
+-- @since 9.0
 class (Typeable a) => Asset a where
+  -- | Configuration for loading an asset.
+  --
+  -- @since 9.0
   type AssetConfig a
 
+  -- | Load an asset from a file path with a configuration.
+  --
+  -- @since 9.0
   loadAsset :: FilePath -> AssetConfig a -> IO a
diff --git a/src/Aztecs/Camera.hs b/src/Aztecs/Camera.hs
--- a/src/Aztecs/Camera.hs
+++ b/src/Aztecs/Camera.hs
@@ -1,8 +1,15 @@
-{-# LANGUAGE Arrows #-}
 {-# LANGUAGE DeriveAnyClass #-}
 {-# LANGUAGE DeriveGeneric #-}
 {-# LANGUAGE TypeApplications #-}
 
+-- |
+-- Module      : Aztecs.Camera
+-- Copyright   : (c) Matt Hunzinger, 2025
+-- License     : BSD-style (see the LICENSE file in the distribution)
+--
+-- Maintainer  : matt@hunzinger.me
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
 module Aztecs.Camera
   ( Camera (..),
     CameraTarget (..),
@@ -14,47 +21,58 @@
 import qualified Aztecs.ECS.Access as A
 import qualified Aztecs.ECS.Query.Reader as Q
 import qualified Aztecs.ECS.System as S
-import Aztecs.Window (Window)
-import Control.Arrow (Arrow (..))
+import Aztecs.Window
+import Control.Arrow
 import Control.DeepSeq
-import GHC.Generics (Generic)
-import Linear (V2 (..))
+import GHC.Generics
+import Linear
 
 -- | Camera component.
+--
+-- @since 9.0
 data Camera = Camera
   { -- | Camera viewport size.
+    --
+    -- @since 9.0
     cameraViewport :: !(V2 Int),
     -- | Camera scale factor.
+    --
+    -- @since 9.0
     cameraScale :: !(V2 Float)
   }
   deriving (Show, Generic, NFData)
 
+-- | @since 9.0
 instance Component Camera
 
 -- | Camera target component.
+--
+-- @since 9.0
 newtype CameraTarget = CameraTarget
   { -- | This camera's target window.
+    --
+    -- @since 9.0
     cameraTargetWindow :: EntityID
   }
   deriving (Eq, Show, Generic, NFData)
 
+-- | @since 9.0
 instance Component CameraTarget
 
 -- | Add `CameraTarget` components to entities with a new `Draw` component.
+--
+-- @since 9.0
 addCameraTargets ::
   ( ArrowQueryReader qr,
     ArrowDynamicQueryReader qr,
-    ArrowReaderSystem qr arr,
-    ArrowQueueSystem b m arr
+    MonadReaderSystem qr s,
+    MonadAccess b m
   ) =>
-  arr () ()
-addCameraTargets = proc () -> do
-  windows <- S.all (Q.entity &&& Q.fetch @_ @Window) -< ()
-  newCameras <- S.filter (Q.entity &&& Q.fetch @_ @Camera) (without @CameraTarget) -< ()
-  S.queue
-    ( \(newCameras, windows) -> case windows of
-        (windowEId, _) : _ -> mapM_ (\(eId, _) -> A.insert eId $ CameraTarget windowEId) newCameras
+  s (m ())
+addCameraTargets = do
+  windows <- S.all () (Q.entity &&& Q.fetch @_ @Window)
+  newCameras <- S.filter () (Q.entity &&& Q.fetch @_ @Camera) (without @CameraTarget)
+  let go = case windows of
+        (windowEId, _) : _ -> mapM_ (\(eId, _) -> A.insert eId . bundle $ CameraTarget windowEId) newCameras
         _ -> return ()
-    )
-    -<
-      (newCameras, windows)
+  return go
diff --git a/src/Aztecs/ECS.hs b/src/Aztecs/ECS.hs
--- a/src/Aztecs/ECS.hs
+++ b/src/Aztecs/ECS.hs
@@ -51,7 +51,10 @@
 -- > main = runSystem_ $ setup >>> S.forever move
 module Aztecs.ECS
   ( Access,
+    AccessT,
+    MonadAccess,
     runAccessT,
+    runAccessT_,
     Bundle,
     MonoidBundle (..),
     DynamicBundle,
@@ -59,6 +62,9 @@
     Component (..),
     EntityID,
     Query,
+    QueryT,
+    QueryReader,
+    QueryReaderT,
     ArrowQueryReader,
     ArrowQuery,
     ArrowDynamicQueryReader,
@@ -67,26 +73,14 @@
     with,
     without,
     System,
-    ArrowReaderSystem,
-    ArrowSystem,
-    ArrowQueueSystem,
-    Schedule,
-    ArrowReaderSchedule,
-    ArrowSchedule,
-    ArrowAccessSchedule,
-    reader,
-    system,
-    delay,
-    forever,
-    forever_,
-    access,
-    runSchedule,
-    runSchedule_,
+    SystemT,
+    MonadReaderSystem,
+    MonadSystem,
     World,
   )
 where
 
-import Aztecs.ECS.Access (Access, runAccessT)
+import Aztecs.ECS.Access
 import Aztecs.ECS.Component (Component (..))
 import Aztecs.ECS.Entity (EntityID)
 import Aztecs.ECS.Query
@@ -96,24 +90,12 @@
     ArrowQueryReader,
     Query,
     QueryFilter,
+    QueryT,
     with,
     without,
   )
-import Aztecs.ECS.Schedule
-  ( ArrowAccessSchedule,
-    ArrowReaderSchedule,
-    ArrowSchedule,
-    Schedule,
-    access,
-    delay,
-    forever,
-    forever_,
-    reader,
-    runSchedule,
-    runSchedule_,
-    system,
-  )
-import Aztecs.ECS.System (ArrowQueueSystem, ArrowReaderSystem, ArrowSystem, System)
+import Aztecs.ECS.Query.Reader (QueryReader, QueryReaderT)
+import Aztecs.ECS.System
 import Aztecs.ECS.World (World)
 import Aztecs.ECS.World.Bundle (Bundle, MonoidBundle (..))
 import Aztecs.ECS.World.Bundle.Dynamic (DynamicBundle, MonoidDynamicBundle (..))
diff --git a/src/Aztecs/ECS/Access.hs b/src/Aztecs/ECS/Access.hs
--- a/src/Aztecs/ECS/Access.hs
+++ b/src/Aztecs/ECS/Access.hs
@@ -3,34 +3,77 @@
 {-# LANGUAGE MultiParamTypeClasses #-}
 {-# LANGUAGE TypeFamilies #-}
 
+-- |
+-- Module      : Aztecs.ECS.Access
+-- Copyright   : (c) Matt Hunzinger, 2025
+-- License     : BSD-style (see the LICENSE file in the distribution)
+--
+-- Maintainer  : matt@hunzinger.me
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
 module Aztecs.ECS.Access
   ( Access,
     AccessT (..),
     MonadAccess (..),
     runAccessT,
+    runAccessT_,
+    system,
   )
 where
 
-import Aztecs.ECS.Access.Class (MonadAccess (..))
+import Aztecs.ECS.Access.Class
+import Aztecs.ECS.Query (QueryT (..))
+import qualified Aztecs.ECS.Query as Q
+import Aztecs.ECS.Query.Dynamic (DynamicQueryT)
+import qualified Aztecs.ECS.Query.Dynamic as Q
+import Aztecs.ECS.Query.Dynamic.Reader (DynamicQueryReaderT)
+import qualified Aztecs.ECS.Query.Dynamic.Reader as Q
+import Aztecs.ECS.Query.Reader
+import Aztecs.ECS.System
 import Aztecs.ECS.World (World (..))
 import qualified Aztecs.ECS.World as W
-import Aztecs.ECS.World.Bundle (Bundle)
-import Control.Monad.Fix (MonadFix)
-import Control.Monad.IO.Class (MonadIO)
-import Control.Monad.Identity (Identity)
-import Control.Monad.State.Strict (MonadState (..), StateT (..))
-import Prelude hiding (all, lookup, map)
+import qualified Aztecs.ECS.World.Archetype as A
+import Aztecs.ECS.World.Archetypes (Node (..))
+import Aztecs.ECS.World.Bundle
+import qualified Aztecs.ECS.World.Entities as E
+import Control.Concurrent.STM
+import Control.DeepSeq
+import Control.Monad.Fix
+import Control.Monad.Identity
+import Control.Monad.Reader
+import Control.Monad.State.Strict
+import qualified Data.Foldable as F
 
+-- | @since 9.0
 type Access = AccessT Identity
 
 -- | Access into the `World`.
+--
+-- @since 9.0
 newtype AccessT m a = AccessT {unAccessT :: StateT World m a}
-  deriving (Functor, Applicative, Monad, MonadFix, MonadIO)
+  deriving (Functor, Applicative, MonadFix, MonadIO)
 
+-- | @since 9.0
+instance (Monad m) => Monad (AccessT m) where
+  a >>= f = AccessT $ do
+    !w <- get
+    (a', w') <- lift $ runAccessT a w
+    put (rnf w' `seq` w')
+    unAccessT $ f a'
+
 -- | Run an `Access` on a `World`, returning the output and updated `World`.
+--
+-- @since 9.0
 runAccessT :: (Functor m) => AccessT m a -> World -> m (a, World)
 runAccessT a = runStateT $ unAccessT a
 
+-- | Run an `Access` on an empty `World`.
+--
+-- @since 9.0
+runAccessT_ :: (Functor m) => AccessT m a -> m a
+runAccessT_ a = fmap fst . runAccessT a $ W.empty
+
+-- | @since 9.0
 instance (Monad m) => MonadAccess Bundle (AccessT m) where
   spawn b = AccessT $ do
     !w <- get
@@ -53,3 +96,81 @@
     !w <- get
     let !(_, w') = W.despawn e w
     put w'
+
+-- | @since 9.0
+instance (Monad m) => MonadReaderSystem (QueryReaderT m) (AccessT m) where
+  all i q = AccessT $ do
+    w <- get
+    let (cIds, cs, dynQ) = runQueryReader q . E.components $ entities w
+    put w {entities = (entities w) {E.components = cs}}
+    unAccessT $ allDyn i cIds dynQ
+  filter i q f = AccessT $ do
+    w <- get
+    let (cIds, cs, dynQ) = runQueryReader q . E.components $ entities w
+        (dynF, cs') = runQueryFilter f cs
+    put w {entities = (entities w) {E.components = cs'}}
+    let f' n =
+          F.all (\cId -> A.member cId $ nodeArchetype n) (filterWith dynF)
+            && F.all (\cId -> not (A.member cId $ nodeArchetype n)) (filterWithout dynF)
+    unAccessT $ filterDyn i cIds dynQ f'
+
+-- | @since 9.0
+instance (Monad m) => MonadSystem (QueryT m) (AccessT m) where
+  map i q = AccessT $ do
+    !w <- get
+    let (rws, cs, dynQ) = runQuery q . E.components $ entities w
+    put w {entities = (entities w) {E.components = cs}}
+    unAccessT $ mapDyn i (Q.reads rws <> Q.writes rws) dynQ
+  mapSingleMaybe i q = AccessT $ do
+    !w <- get
+    let (rws, cs, dynQ) = runQuery q . E.components $ entities w
+    put w {entities = (entities w) {E.components = cs}}
+    unAccessT $ mapSingleMaybeDyn i (Q.reads rws <> Q.writes rws) dynQ
+  filterMap i q f = AccessT $ do
+    !w <- get
+    let (rws, cs, dynQ) = runQuery q . E.components $ entities w
+        (dynF, cs') = runQueryFilter f cs
+    put w {entities = (entities w) {E.components = cs'}}
+    let f' n =
+          F.all (\cId -> A.member cId $ nodeArchetype n) (filterWith dynF)
+            && F.all (\cId -> not (A.member cId $ nodeArchetype n)) (filterWithout dynF)
+    unAccessT $ filterMapDyn i (Q.reads rws <> Q.writes rws) f' dynQ
+
+-- | @since 9.0
+instance (Monad m) => MonadDynamicReaderSystem (DynamicQueryReaderT m) (AccessT m) where
+  allDyn i cIds q = AccessT $ do
+    !w <- get
+    lift . Q.allDyn cIds i q $ entities w
+  filterDyn i cIds q f = AccessT $ do
+    !w <- get
+    lift . Q.filterDyn cIds i f q $ entities w
+
+-- | @since 9.0
+instance (Monad m) => MonadDynamicSystem (DynamicQueryT m) (AccessT m) where
+  mapDyn i cIds q = AccessT $ do
+    !w <- get
+    (as, es) <- lift . Q.mapDyn cIds i q $ entities w
+    put w {entities = es}
+    return as
+  mapSingleMaybeDyn i cIds q = AccessT $ do
+    !w <- get
+    (res, es) <- lift . Q.mapSingleMaybeDyn cIds i q $ entities w
+    put w {entities = es}
+    return res
+  filterMapDyn i cIds f q = AccessT $ do
+    !w <- get
+    (as, es) <- lift . Q.filterMapDyn cIds i f q $ entities w
+    put w {entities = es}
+    return as
+
+-- | Run a `System`.
+--
+-- @since 9.0
+system :: System a -> AccessT IO a
+system s = AccessT $ do
+  !w <- get
+  esVar <- lift . newTVarIO $ entities w
+  a <- lift . atomically $ runReaderT (runSystemT s) esVar
+  es <- lift $ readTVarIO esVar
+  put w {entities = es}
+  return a
diff --git a/src/Aztecs/ECS/Access/Class.hs b/src/Aztecs/ECS/Access/Class.hs
--- a/src/Aztecs/ECS/Access/Class.hs
+++ b/src/Aztecs/ECS/Access/Class.hs
@@ -1,32 +1,52 @@
-{-# LANGUAGE FlexibleContexts #-}
 {-# LANGUAGE FunctionalDependencies #-}
 
+-- |
+-- Module      : Aztecs.ECS.Access.Class
+-- Copyright   : (c) Matt Hunzinger, 2025
+-- License     : BSD-style (see the LICENSE file in the distribution)
+--
+-- Maintainer  : matt@hunzinger.me
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
 module Aztecs.ECS.Access.Class (MonadAccess (..)) where
 
-import Aztecs.ECS.Component (Component (..))
-import Aztecs.ECS.Entity (EntityID (..))
-import Aztecs.ECS.World.Bundle (MonoidBundle (..))
-import Prelude hiding (all, lookup, map)
+import Aztecs.ECS.Component
+import Aztecs.ECS.Entity
+import Aztecs.ECS.World.Bundle.Class
 
 -- | Monadic access to a `World`.
+--
+-- @since 9.0
 class (MonoidBundle b, Monad m) => MonadAccess b m | m -> b where
   -- | Spawn an entity with a component.
+  --
+  -- @since 9.0
   spawn :: b -> m EntityID
 
   -- | Spawn an entity with a component.
+  --
+  -- @since 9.0
   spawn_ :: b -> m ()
   spawn_ c = do
     _ <- spawn c
     return ()
 
   -- | Insert a component into an entity.
-  insert :: (Component a) => EntityID -> a -> m ()
+  --
+  -- @since 9.0
+  insert :: EntityID -> b -> m ()
 
   -- | Lookup a component on an entity.
+  --
+  -- @since 9.0
   lookup :: (Component a) => EntityID -> m (Maybe a)
 
   -- | Remove a component from an entity.
+  --
+  -- @since 9.0
   remove :: (Component a) => EntityID -> m (Maybe a)
 
   -- | Despawn an entity.
+  --
+  -- @since 9.0
   despawn :: EntityID -> m ()
diff --git a/src/Aztecs/ECS/Component.hs b/src/Aztecs/ECS/Component.hs
--- a/src/Aztecs/ECS/Component.hs
+++ b/src/Aztecs/ECS/Component.hs
@@ -3,21 +3,39 @@
 {-# LANGUAGE GeneralizedNewtypeDeriving #-}
 {-# LANGUAGE TypeFamilies #-}
 
-module Aztecs.ECS.Component (Component (..), ComponentID (..)) where
+-- |
+-- Module      : Aztecs.ECS.Component
+-- Copyright   : (c) Matt Hunzinger, 2025
+-- License     : BSD-style (see the LICENSE file in the distribution)
+--
+-- Maintainer  : matt@hunzinger.me
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
+module Aztecs.ECS.Component where
 
-import Aztecs.ECS.World.Storage (Storage)
-import Control.DeepSeq (NFData)
-import Data.Kind (Type)
-import Data.Typeable (Typeable)
-import GHC.Generics (Generic)
+import Aztecs.ECS.World.Storage
+import Control.DeepSeq
+import Data.Typeable
+import GHC.Generics
 
--- | Component ID.
-newtype ComponentID = ComponentID {unComponentId :: Int}
+-- | Unique component identifier.
+--
+-- @since 9.0
+newtype ComponentID = ComponentID
+  { -- | Unique integer identifier.
+    --
+    -- @since 9.0
+    unComponentId :: Int
+  }
   deriving (Eq, Ord, Show, Generic, NFData)
 
 -- | Component that can be stored in the `World`.
+--
+-- @since 9.0
 class (Typeable a, Storage a (StorageT a)) => Component a where
   -- | `Storage` of this component.
-  type StorageT a :: Type
+  --
+  -- @since 9.0
+  type StorageT a
 
   type StorageT a = [a]
diff --git a/src/Aztecs/ECS/Entity.hs b/src/Aztecs/ECS/Entity.hs
--- a/src/Aztecs/ECS/Entity.hs
+++ b/src/Aztecs/ECS/Entity.hs
@@ -1,11 +1,26 @@
 {-# LANGUAGE DeriveGeneric #-}
 {-# LANGUAGE GeneralizedNewtypeDeriving #-}
 
+-- |
+-- Module      : Aztecs.ECS.Entity
+-- Copyright   : (c) Matt Hunzinger, 2025
+-- License     : BSD-style (see the LICENSE file in the distribution)
+--
+-- Maintainer  : matt@hunzinger.me
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
 module Aztecs.ECS.Entity (EntityID (..)) where
 
-import Control.DeepSeq (NFData)
-import GHC.Generics (Generic)
+import Control.DeepSeq
+import GHC.Generics
 
--- | Entity ID.
-newtype EntityID = EntityID {unEntityId :: Int}
+-- | Unique entity identifier.
+--
+-- @since 9.0
+newtype EntityID = EntityID
+  { -- | Unique integer identifier.
+    --
+    -- @since 9.0
+    unEntityId :: Int
+  }
   deriving (Eq, Ord, Show, Generic, NFData)
diff --git a/src/Aztecs/ECS/Query.hs b/src/Aztecs/ECS/Query.hs
--- a/src/Aztecs/ECS/Query.hs
+++ b/src/Aztecs/ECS/Query.hs
@@ -1,13 +1,43 @@
+{-# LANGUAGE AllowAmbiguousTypes #-}
+{-# LANGUAGE BangPatterns #-}
 {-# LANGUAGE DeriveFunctor #-}
+{-# LANGUAGE FlexibleInstances #-}
 {-# LANGUAGE InstanceSigs #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
 {-# LANGUAGE ScopedTypeVariables #-}
 {-# LANGUAGE TupleSections #-}
 {-# LANGUAGE TypeApplications #-}
 {-# LANGUAGE TypeFamilies #-}
 
+-- |
+-- Module      : Aztecs.ECS.Query
+-- Copyright   : (c) Matt Hunzinger, 2025
+-- License     : BSD-style (see the LICENSE file in the distribution)
+--
+-- Maintainer  : matt@hunzinger.me
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
+--
+-- Query for matching entities.
+--
+-- === Do notation:
+-- > move :: (ArrowQuery arr) => arr () Position
+-- > move = proc () -> do
+-- >   Velocity v <- Q.fetch -< ()
+-- >   Position p <- Q.fetch -< ()
+-- >   Q.set -< Position $ p + v
+--
+-- === Arrow combinators:
+-- > move :: (ArrowQuery arr) => arr () Position
+-- > move = Q.fetch &&& Q.fetch >>> arr (\(Position p, Velocity v) -> Position $ p + v) >>> Q.set
+--
+-- === Applicative combinators:
+-- > move :: (ArrowQuery arr) => arr () Position
+-- > move = (,) <$> Q.fetch <*> Q.fetch >>> arr (\(Position p, Velocity v) -> Position $ p + v) >>> Q.set
 module Aztecs.ECS.Query
   ( -- * Queries
-    Query (..),
+    Query,
+    QueryT (..),
     ArrowQueryReader (..),
     ArrowQuery (..),
     ArrowDynamicQueryReader (..),
@@ -15,7 +45,14 @@
 
     -- ** Running
     all,
+    all',
+    single,
+    single',
+    singleMaybe,
+    singleMaybe',
     map,
+    mapSingle,
+    mapSingleMaybe,
 
     -- ** Conversion
     fromReader,
@@ -33,102 +70,163 @@
 where
 
 import Aztecs.ECS.Component
-import Aztecs.ECS.Query.Class (ArrowQuery (..))
-import Aztecs.ECS.Query.Dynamic (DynamicQuery (..), fromDynReader, mapDyn, toDynReader)
-import Aztecs.ECS.Query.Dynamic.Class (ArrowDynamicQuery (..))
-import Aztecs.ECS.Query.Dynamic.Reader.Class (ArrowDynamicQueryReader (..))
-import Aztecs.ECS.Query.Reader (QueryFilter (..), QueryReader (..), with, without)
+import Aztecs.ECS.Query.Class
+import Aztecs.ECS.Query.Dynamic
+import Aztecs.ECS.Query.Reader (QueryFilter (..), QueryReaderT (..), with, without)
 import qualified Aztecs.ECS.Query.Reader as QR
-import Aztecs.ECS.Query.Reader.Class (ArrowQueryReader (..))
+import Aztecs.ECS.Query.Reader.Class
 import Aztecs.ECS.World.Components (Components)
 import qualified Aztecs.ECS.World.Components as CS
 import Aztecs.ECS.World.Entities (Entities (..))
-import Control.Arrow (Arrow (..), ArrowChoice (..))
-import Control.Category (Category (..))
+import Control.Arrow
+import Control.Category
+import Control.Monad.Identity
 import Data.Set (Set)
 import qualified Data.Set as Set
+import GHC.Stack
 import Prelude hiding (all, id, map, reads, (.))
 
+-- | @since 9.0
+type Query i = QueryT Identity i
+
 -- | Query for matching entities.
 --
--- === Do notation:
--- > move :: (ArrowQuery arr) => arr () Position
--- > move = proc () -> do
--- >   Velocity v <- Q.fetch -< ()
--- >   Position p <- Q.fetch -< ()
--- >   Q.set -< Position $ p + v
---
--- === Arrow combinators:
--- > move :: (ArrowQuery arr) => arr () Position
--- > move = Q.fetch &&& Q.fetch >>> arr (\(Position p, Velocity v) -> Position $ p + v) >>> Q.set
---
--- === Applicative combinators:
--- > move :: (ArrowQuery arr) => arr () Position
--- > move = (,) <$> Q.fetch <*> Q.fetch >>> arr (\(Position p, Velocity v) -> Position $ p + v) >>> Q.set
-newtype Query i o = Query {runQuery :: Components -> (ReadsWrites, Components, DynamicQuery i o)}
+-- @since 9.0
+newtype QueryT m i o = Query
+  { -- | Run a query, producing a `DynamicQueryT`.
+    --
+    -- @since 9.0
+    runQuery :: Components -> (ReadsWrites, Components, DynamicQueryT m i o)
+  }
   deriving (Functor)
 
-instance Applicative (Query i) where
+-- | @since 9.0
+instance (Monad m) => Applicative (QueryT m i) where
+  {-# INLINE pure #-}
   pure a = Query (mempty,,pure a)
+  {-# INLINE (<*>) #-}
   (Query f) <*> (Query g) = Query $ \cs ->
-    let (cIdsG, cs', aQS) = g cs
-        (cIdsF, cs'', bQS) = f cs'
+    let !(cIdsG, cs', aQS) = g cs
+        !(cIdsF, cs'', bQS) = f cs'
      in (cIdsG <> cIdsF, cs'', bQS <*> aQS)
 
-instance Category Query where
+-- | @since 9.0
+instance (Monad m) => Category (QueryT m) where
+  {-# INLINE id #-}
   id = Query (mempty,,id)
+  {-# INLINE (.) #-}
   (Query f) . (Query g) = Query $ \cs ->
-    let (cIdsG, cs', aQS) = g cs
-        (cIdsF, cs'', bQS) = f cs'
+    let !(cIdsG, cs', aQS) = g cs
+        !(cIdsF, cs'', bQS) = f cs'
      in (cIdsG <> cIdsF, cs'', bQS . aQS)
 
-instance Arrow Query where
+-- | @since 9.0
+instance (Monad m) => Arrow (QueryT m) where
+  {-# INLINE arr #-}
   arr f = Query (mempty,,arr f)
-  first (Query f) = Query $ \comps -> let (cIds, comps', qS) = f comps in (cIds, comps', first qS)
+  {-# INLINE first #-}
+  first (Query f) = Query $ \cs -> let !(cIds, cs', qS) = f cs in (cIds, cs', first qS)
 
-instance ArrowChoice Query where
-  left (Query f) = Query $ \comps -> let (cIds, comps', qS) = f comps in (cIds, comps', left qS)
+-- |  @since 9.0
+instance (Monad m) => ArrowChoice (QueryT m) where
+  {-# INLINE left #-}
+  left (Query f) = Query $ \cs -> let !(cIds, cs', qS) = f cs in (cIds, cs', left qS)
 
-instance ArrowQueryReader Query where
+-- | @since 9.0
+instance (Monad m) => ArrowQueryReader (QueryT m) where
+  {-# INLINE fetch #-}
   fetch = fromReader fetch
+  {-# INLINE fetchMaybe #-}
   fetchMaybe = fromReader fetchMaybe
 
-instance ArrowDynamicQueryReader Query where
+-- | @since 9.0
+instance (Monad m) => ArrowDynamicQueryReader (QueryT m) where
+  {-# INLINE entity #-}
   entity = fromReader entity
+  {-# INLINE fetchDyn #-}
   fetchDyn = fromReader . fetchDyn
+  {-# INLINE fetchMaybeDyn #-}
   fetchMaybeDyn = fromReader . fetchMaybeDyn
 
-instance ArrowDynamicQuery Query where
+-- | @since 9.0
+instance (Monad m) => ArrowDynamicQuery m (QueryT m) where
+  {-# INLINE adjustDyn #-}
+  adjustDyn f cId = Query (ReadsWrites Set.empty (Set.singleton cId),,adjustDyn f cId)
+  {-# INLINE adjustDyn_ #-}
+  adjustDyn_ f cId = Query (ReadsWrites Set.empty (Set.singleton cId),,adjustDyn_ f cId)
+  {-# INLINE adjustDynM #-}
+  adjustDynM f cId = Query (ReadsWrites Set.empty (Set.singleton cId),,adjustDynM f cId)
+  {-# INLINE setDyn #-}
   setDyn cId = Query (ReadsWrites Set.empty (Set.singleton cId),,setDyn cId)
 
-instance ArrowQuery Query where
-  set :: forall a. (Component a) => Query a a
-  set = Query $ \cs ->
-    let (cId, cs') = CS.insert @a cs
-     in (ReadsWrites Set.empty (Set.singleton cId), cs', setDyn cId)
+-- | @since 9.0
+instance (Monad m) => ArrowQuery m (QueryT m) where
+  {-# INLINE adjust #-}
+  adjust :: forall i a. (Component a) => (i -> a -> a) -> QueryT m i a
+  adjust f = fromDyn @a $ adjustDyn f
 
-fromReader :: QueryReader i o -> Query i o
+  {-# INLINE adjust_ #-}
+  adjust_ :: forall i a. (Component a) => (i -> a -> a) -> QueryT m i ()
+  adjust_ f = fromDyn @a $ adjustDyn_ f
+
+  {-# INLINE adjustM #-}
+  adjustM :: forall i a. (Component a, Monad m) => (i -> a -> m a) -> QueryT m i a
+  adjustM f = fromDyn @a $ adjustDynM f
+
+  {-# INLINE set #-}
+  set :: forall a. (Component a) => QueryT m a a
+  set = fromDyn @a $ setDyn
+
+-- | Convert a `DynamicQueryT` to a `Query`.
+--
+-- @since 9.0
+{-# INLINE fromDyn #-}
+fromDyn :: forall a m i o. (Component a) => (ComponentID -> DynamicQueryT m i o) -> QueryT m i o
+fromDyn f = Query $ \cs ->
+  let !(cId, cs') = CS.insert @a cs in (ReadsWrites (Set.singleton cId) (Set.singleton cId), cs', f cId)
+
+-- | Convert a `QueryReader` to a `Query`.
+--
+-- @since 9.0
+{-# INLINE fromReader #-}
+fromReader :: (Monad m) => QueryReaderT m i o -> QueryT m i o
 fromReader (QueryReader f) = Query $ \cs ->
-  let (cIds, cs', dynQ) = f cs in (ReadsWrites cIds Set.empty, cs', fromDynReader dynQ)
+  let !(cIds, cs', dynQ) = f cs in (ReadsWrites cIds Set.empty, cs', fromDynReader dynQ)
 
-toReader :: Query i o -> QueryReader i o
+-- | Convert a `Query` to a `QueryReader`.
+--
+-- @since 9.0
+{-# INLINE toReader #-}
+toReader :: (Functor m) => QueryT m i o -> QueryReaderT m i o
 toReader (Query f) = QueryReader $ \cs ->
-  let (rws, cs', dynQ) = f cs in (reads rws, cs', toDynReader dynQ)
+  let !(rws, cs', dynQ) = f cs in (reads rws, cs', toDynReader dynQ)
 
 -- | Reads and writes of a `Query`.
+--
+-- @since 9.0
 data ReadsWrites = ReadsWrites
-  { reads :: !(Set ComponentID),
+  { -- | Component IDs being read.
+    --
+    -- @since 9.0
+    reads :: !(Set ComponentID),
+    -- | Component IDs being written.
+    --
+    -- @since 9.0
     writes :: !(Set ComponentID)
   }
   deriving (Show)
 
+-- | @since 9.0
 instance Semigroup ReadsWrites where
   ReadsWrites r1 w1 <> ReadsWrites r2 w2 = ReadsWrites (r1 <> r2) (w1 <> w2)
 
+-- | @since 9.0
 instance Monoid ReadsWrites where
   mempty = ReadsWrites mempty mempty
 
 -- | `True` if the reads and writes of two `Query`s overlap.
+--
+-- @since 9.0
 disjoint :: ReadsWrites -> ReadsWrites -> Bool
 disjoint a b =
   Set.disjoint (reads a) (writes b)
@@ -136,13 +234,76 @@
     || Set.disjoint (writes b) (writes a)
 
 -- | Match all entities.
-all :: i -> Query i a -> Entities -> ([a], Entities)
+--
+-- @since 9.0
+{-# INLINE all #-}
+all :: (Monad m) => i -> QueryT m i a -> Entities -> (m [a], Entities)
 all i = QR.all i . toReader
 
+-- | Match all entities.
+--
+-- @since 9.0
+{-# INLINE all' #-}
+all' :: (Monad m) => i -> QueryT m i a -> Entities -> (m [a], Components)
+all' i = QR.all' i . toReader
+
+-- | Match a single entity.
+--
+-- @since 9.0
+{-# INLINE single #-}
+single :: (HasCallStack) => i -> Query i a -> Entities -> (a, Entities)
+single i = QR.single i . toReader
+
+-- | Match a single entity.
+--
+-- @since 9.0
+{-# INLINE single' #-}
+single' :: (HasCallStack) => i -> Query i a -> Entities -> (a, Components)
+single' i = QR.single' i . toReader
+
+-- | Match a single entity, or `Nothing`.
+--
+-- @since 9.0
+{-# INLINE singleMaybe #-}
+singleMaybe :: i -> Query i a -> Entities -> (Maybe a, Entities)
+singleMaybe i = QR.singleMaybe i . toReader
+
+-- | Match a single entity, or `Nothing`.
+--
+-- @since 9.0
+{-# INLINE singleMaybe' #-}
+singleMaybe' :: i -> Query i a -> Entities -> (Maybe a, Components)
+singleMaybe' i = QR.singleMaybe' i . toReader
+
 -- | Map all matched entities.
-map :: i -> Query i a -> Entities -> ([a], Entities)
-map i q es =
-  let (rws, cs', dynQ) = runQuery q (components es)
-      cIds = reads rws <> writes rws
-      (as, es') = mapDyn cIds i dynQ es
-   in (as, es' {components = cs'})
+--
+-- @since 9.0
+{-# INLINE map #-}
+map :: (Monad m) => i -> QueryT m i o -> Entities -> m ([o], Entities)
+map i q es = do
+  let !(rws, cs', dynQ) = runQuery q $ components es
+      !cIds = reads rws <> writes rws
+  (as, es') <- mapDyn cIds i dynQ es
+  return (as, es' {components = cs'})
+
+-- | Map a single matched entity.
+--
+-- @since 9.0
+{-# INLINE mapSingle #-}
+mapSingle :: (HasCallStack, Monad m) => i -> QueryT m i a -> Entities -> m (a, Entities)
+mapSingle i q es = do
+  let !(rws, cs', dynQ) = runQuery q $ components es
+      !cIds = reads rws <> writes rws
+  (as, es') <- mapSingleDyn cIds i dynQ es
+  return (as, es' {components = cs'})
+
+-- | Map a single matched entity, or `Nothing`.
+--
+-- @since 9.0
+{-# INLINE mapSingleMaybe #-}
+mapSingleMaybe :: (Monad m) => i -> QueryT m i a -> Entities -> m (Maybe a, Entities)
+mapSingleMaybe i q es = do
+  let !(rws, cs', dynQ) = runQuery q $ components es
+      !cIds = reads rws <> writes rws
+  (as, es') <- mapSingleMaybeDyn cIds i dynQ es
+  return (as, es' {components = cs'})
diff --git a/src/Aztecs/ECS/Query/Class.hs b/src/Aztecs/ECS/Query/Class.hs
--- a/src/Aztecs/ECS/Query/Class.hs
+++ b/src/Aztecs/ECS/Query/Class.hs
@@ -1,9 +1,43 @@
+{-# LANGUAGE AllowAmbiguousTypes #-}
+{-# LANGUAGE FunctionalDependencies #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
+
+-- |
+-- Module      : Aztecs.ECS.Query.Reader.Class
+-- Copyright   : (c) Matt Hunzinger, 2025
+-- License     : BSD-style (see the LICENSE file in the distribution)
+--
+-- Maintainer  : matt@hunzinger.me
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
 module Aztecs.ECS.Query.Class (ArrowQuery (..)) where
 
 import Aztecs.ECS.Component
 import Aztecs.ECS.Query.Reader.Class (ArrowQueryReader)
+import Control.Arrow
 
 -- | Arrow for queries that can update entities.
-class (ArrowQueryReader arr) => ArrowQuery arr where
+--
+-- @since 9.0
+class (ArrowQueryReader arr) => ArrowQuery m arr | arr -> m where
+  -- | Adjust a `Component` by its type.
+  --
+  -- @since 9.0
+  adjust :: (Component a) => (i -> a -> a) -> arr i a
+
+  -- | Adjust a `Component` by its type, ignoring any output.
+  --
+  -- @since 9.0
+  adjust_ :: (Component a) => (i -> a -> a) -> arr i ()
+  adjust_ f = adjust @m f >>> arr (const ())
+
+  -- | Adjust a `Component` by its type with some `Monad` @m@.
+  --
+  -- @since 9.0
+  adjustM :: (Component a) => (i -> a -> m a) -> arr i a
+
   -- | Set a `Component` by its type.
+  --
+  -- @since 9.0
   set :: (Component a) => arr a a
diff --git a/src/Aztecs/ECS/Query/Dynamic.hs b/src/Aztecs/ECS/Query/Dynamic.hs
--- a/src/Aztecs/ECS/Query/Dynamic.hs
+++ b/src/Aztecs/ECS/Query/Dynamic.hs
@@ -1,10 +1,21 @@
 {-# LANGUAGE BangPatterns #-}
 {-# LANGUAGE DeriveFunctor #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
 {-# LANGUAGE TypeFamilies #-}
 
+-- |
+-- Module      : Aztecs.ECS.Query.Dynamic
+-- Copyright   : (c) Matt Hunzinger, 2025
+-- License     : BSD-style (see the LICENSE file in the distribution)
+--
+-- Maintainer  : matt@hunzinger.me
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
 module Aztecs.ECS.Query.Dynamic
   ( -- * Dynamic queries
-    DynamicQuery (..),
+    DynamicQuery,
+    DynamicQueryT (..),
     ArrowDynamicQueryReader (..),
     ArrowDynamicQuery (..),
 
@@ -14,93 +25,192 @@
 
     -- ** Running
     mapDyn,
+    filterMapDyn,
+    mapSingleDyn,
+    mapSingleMaybeDyn,
 
     -- * Dynamic query filters
     DynamicQueryFilter (..),
   )
 where
 
-import Aztecs.ECS.Component (ComponentID)
-import Aztecs.ECS.Entity (EntityID)
-import Aztecs.ECS.Query.Dynamic.Class (ArrowDynamicQuery (..))
-import Aztecs.ECS.Query.Dynamic.Reader (DynamicQueryFilter (..), DynamicQueryReader (..))
-import Aztecs.ECS.Query.Dynamic.Reader.Class (ArrowDynamicQueryReader (..))
+import Aztecs.ECS.Component
+import Aztecs.ECS.Query.Dynamic.Class
+import Aztecs.ECS.Query.Dynamic.Reader
 import Aztecs.ECS.World.Archetype (Archetype)
 import qualified Aztecs.ECS.World.Archetype as A
 import Aztecs.ECS.World.Archetypes (Node (..))
 import qualified Aztecs.ECS.World.Archetypes as AS
 import Aztecs.ECS.World.Entities (Entities (..))
-import Control.Arrow (Arrow (..), ArrowChoice (..))
-import Control.Category (Category (..))
+import Control.Arrow
+import Control.Category
+import Control.Monad.Identity
 import Data.Either (partitionEithers)
 import Data.Foldable
 import qualified Data.Map as Map
 import Data.Set (Set)
 import qualified Data.Set as Set
+import GHC.Stack (HasCallStack)
 import Prelude hiding ((.))
 
+-- | @since 9.0
+type DynamicQuery = DynamicQueryT Identity
+
 -- | Dynamic query for components by ID.
-newtype DynamicQuery i o
-  = DynamicQuery {runDynQuery :: [i] -> [EntityID] -> Archetype -> ([o], Archetype)}
+--
+-- @since 9.0
+newtype DynamicQueryT m i o
+  = DynamicQuery
+  { -- | Run a dynamic query with a list of inputs with length equal to the number of entities in the `Archetype`.
+    -- This is an internal function that should typically not be used directly.
+    --
+    -- @since 9.0
+    runDynQuery :: [i] -> Archetype -> m ([o], Archetype)
+  }
   deriving (Functor)
 
-instance Applicative (DynamicQuery i) where
-  pure a = DynamicQuery $ \_ es arch -> (replicate (length es) a, arch)
-
-  f <*> g = DynamicQuery $ \i es arch ->
-    let (as, arch') = runDynQuery g i es arch
-        (fs, arch'') = runDynQuery f i es arch'
-     in (zipWith ($) fs as, arch'')
-
-instance Category DynamicQuery where
-  id = DynamicQuery $ \as _ arch -> (as, arch)
+-- | @since 9.0
+instance (Monad m) => Applicative (DynamicQueryT m i) where
+  {-# INLINE pure #-}
+  pure a = DynamicQuery $ \is arch -> pure (replicate (length is) a, arch)
+  {-# INLINE (<*>) #-}
+  f <*> g = DynamicQuery $ \i arch -> do
+    (as, arch') <- runDynQuery g i arch
+    (fs, arch'') <- runDynQuery f i arch'
+    return (zipWith ($) fs as, arch'')
 
-  f . g = DynamicQuery $ \i es arch ->
-    let (as, arch') = runDynQuery g i es arch in runDynQuery f as es arch'
+-- | @since 9.0
+instance (Monad m) => Category (DynamicQueryT m) where
+  {-# INLINE id #-}
+  id = DynamicQuery $ curry pure
+  {-# INLINE (.) #-}
+  f . g = DynamicQuery $ \i arch -> do
+    (as, arch') <- runDynQuery g i arch
+    runDynQuery f as arch'
 
-instance Arrow DynamicQuery where
-  arr f = DynamicQuery $ \bs _ arch -> (fmap f bs, arch)
-  first f = DynamicQuery $ \bds es arch ->
-    let (bs, ds) = unzip bds
-        (cs, arch') = runDynQuery f bs es arch
-     in (zip cs ds, arch')
+-- | @since 9.0
+instance (Monad m) => Arrow (DynamicQueryT m) where
+  {-# INLINE arr #-}
+  arr f = DynamicQuery $ \bs arch -> pure (fmap f bs, arch)
+  {-# INLINE first #-}
+  first f = DynamicQuery $ \bds arch -> do
+    let !(bs, ds) = unzip bds
+    (cs, arch') <- runDynQuery f bs arch
+    return (zip cs ds, arch')
 
-instance ArrowChoice DynamicQuery where
-  left f = DynamicQuery $ \eds es arch ->
-    let (es', ds) = partitionEithers eds
-        (cs, arch') = runDynQuery f es' es arch
-     in (fmap Left cs ++ fmap Right ds, arch')
+-- | @since 9.0
+instance (Monad m) => ArrowChoice (DynamicQueryT m) where
+  {-# INLINE left #-}
+  left f = DynamicQuery $ \eds arch -> do
+    let !(es', ds) = partitionEithers eds
+    (cs, arch') <- runDynQuery f es' arch
+    return (fmap Left cs ++ fmap Right ds, arch')
 
-instance ArrowDynamicQueryReader DynamicQuery where
+-- | @since 9.0
+instance (Monad m) => ArrowDynamicQueryReader (DynamicQueryT m) where
+  {-# INLINE entity #-}
   entity = fromDynReader entity
+  {-# INLINE fetchDyn #-}
   fetchDyn = fromDynReader . fetchDyn
+  {-# INLINE fetchMaybeDyn #-}
   fetchMaybeDyn = fromDynReader . fetchMaybeDyn
 
-instance ArrowDynamicQuery DynamicQuery where
-  setDyn cId = DynamicQuery $ \is _ arch ->
-    let !arch' = A.insertAscList cId is arch in (is, arch')
+-- | @since 9.0
+instance (Monad m) => ArrowDynamicQuery m (DynamicQueryT m) where
+  {-# INLINE adjustDyn #-}
+  adjustDyn f cId = DynamicQuery $ \is arch -> pure $ A.zipWith is f cId arch
 
-fromDynReader :: DynamicQueryReader i o -> DynamicQuery i o
-fromDynReader q = DynamicQuery $ \is es arch ->
-  let os = runDynQueryReader' q is es arch in (os, arch)
+  {-# INLINE adjustDyn_ #-}
+  adjustDyn_ f cId = DynamicQuery $ \is arch -> pure (repeat (), A.zipWith_ is f cId arch)
 
-toDynReader :: DynamicQuery i o -> DynamicQueryReader i o
-toDynReader q = DynamicQueryReader $ \is es arch -> fst $ runDynQuery q is es arch
+  {-# INLINE adjustDynM #-}
+  adjustDynM f cId = DynamicQuery $ \is arch -> A.zipWithM is f cId arch
 
+  {-# INLINE setDyn #-}
+  setDyn cId = DynamicQuery $ \is arch -> pure (is, A.insertAscList cId is arch)
+
+-- | Convert a `DynamicQueryReaderT` to a `DynamicQueryT`.
+--
+-- @since 9.0
+{-# INLINE fromDynReader #-}
+fromDynReader :: (Monad m) => DynamicQueryReaderT m i o -> DynamicQueryT m i o
+fromDynReader q = DynamicQuery $ \is arch -> do
+  !os <- runDynQueryReader' q is arch
+  return (os, arch)
+
+-- | Convert a `DynamicQueryT` to a `DynamicQueryReaderT`.
+--
+-- @since 9.0
+{-# INLINE toDynReader #-}
+toDynReader :: (Functor m) => DynamicQueryT m i o -> DynamicQueryReaderT m i o
+toDynReader q = DynamicQueryReader $ \is arch -> fst <$> runDynQuery q is arch
+
 -- | Map all matched entities.
-mapDyn :: Set ComponentID -> i -> DynamicQuery i a -> Entities -> ([a], Entities)
+--
+-- @since 9.0
+{-# INLINE mapDyn #-}
+mapDyn :: (Monad m) => Set ComponentID -> i -> DynamicQueryT m i a -> Entities -> m ([a], Entities)
 mapDyn cIds i q es =
-  let (as, es') =
-        if Set.null cIds
-          then (fst $ go (Map.keys $ entities es) A.empty, es)
-          else
-            foldl'
-              ( \(acc, esAcc) (aId, n) ->
-                  let (as', arch') = go (Set.toList . A.entities $ nodeArchetype n) (nodeArchetype n)
-                      nodes = Map.insert aId n {nodeArchetype = arch'} (AS.nodes $ archetypes esAcc)
-                   in (as' ++ acc, esAcc {archetypes = (archetypes esAcc) {AS.nodes = nodes}})
-              )
-              ([], es)
-              (Map.toList $ AS.find cIds (archetypes es))
-      go = runDynQuery q (repeat i)
-   in (as, es')
+  let go = runDynQuery q (repeat i)
+   in if Set.null cIds
+        then do
+          (as, _) <- go A.empty {A.entities = Map.keysSet $ entities es}
+          return (as, es)
+        else
+          let go' (acc, esAcc) (aId, n) = do
+                (as', arch') <- go $ nodeArchetype n
+                let !nodes = Map.insert aId n {nodeArchetype = arch'} . AS.nodes $ archetypes esAcc
+                return (as' ++ acc, esAcc {archetypes = (archetypes esAcc) {AS.nodes = nodes}})
+           in foldlM go' ([], es) $ Map.toList . AS.find cIds $ archetypes es
+
+-- | Map all matched entities.
+--
+-- @since 9.0
+{-# INLINE filterMapDyn #-}
+filterMapDyn :: (Monad m) => Set ComponentID -> i -> (Node -> Bool) -> DynamicQueryT m i a -> Entities -> m ([a], Entities)
+filterMapDyn cIds i f q es =
+  let go = runDynQuery q (repeat i)
+   in if Set.null cIds
+        then do
+          (as, _) <- go A.empty {A.entities = Map.keysSet $ entities es}
+          return (as, es)
+        else
+          let go' (acc, esAcc) (aId, n) = do
+                (as', arch') <- go $ nodeArchetype n
+                let !nodes = Map.insert aId n {nodeArchetype = arch'} . AS.nodes $ archetypes esAcc
+                return (as' ++ acc, esAcc {archetypes = (archetypes esAcc) {AS.nodes = nodes}})
+           in foldlM go' ([], es) $ Map.toList . Map.filter f . AS.find cIds $ archetypes es
+
+-- | Map a single matched entity.
+--
+-- @since 9.0
+mapSingleDyn :: (HasCallStack, Monad m) => Set ComponentID -> i -> DynamicQueryT m i a -> Entities -> m (a, Entities)
+mapSingleDyn cIds i q es = do
+  res <- mapSingleMaybeDyn cIds i q es
+  return $ case res of
+    (Just a, es') -> (a, es')
+    _ -> error "mapSingleDyn: expected single matching entity"
+
+-- | Map a single matched entity, or @Nothing@.
+--
+-- @since 9.0
+{-# INLINE mapSingleMaybeDyn #-}
+mapSingleMaybeDyn :: (Monad m) => Set ComponentID -> i -> DynamicQueryT m i a -> Entities -> m (Maybe a, Entities)
+mapSingleMaybeDyn cIds i q es =
+  if Set.null cIds
+    then case Map.keys $ entities es of
+      [eId] -> do
+        res <- runDynQuery q [i] $ A.singleton eId
+        return $ case res of
+          ([a], _) -> (Just a, es)
+          _ -> (Nothing, es)
+      _ -> pure (Nothing, es)
+    else case Map.toList $ AS.find cIds $ archetypes es of
+      [(aId, n)] -> do
+        res <- runDynQuery q [i] (AS.nodeArchetype n)
+        return $ case res of
+          ([a], arch') ->
+            let nodes = Map.insert aId n {nodeArchetype = arch'} . AS.nodes $ archetypes es
+             in (Just a, es {archetypes = (archetypes es) {AS.nodes = nodes}})
+          _ -> (Nothing, es)
+      _ -> pure (Nothing, es)
diff --git a/src/Aztecs/ECS/Query/Dynamic/Class.hs b/src/Aztecs/ECS/Query/Dynamic/Class.hs
--- a/src/Aztecs/ECS/Query/Dynamic/Class.hs
+++ b/src/Aztecs/ECS/Query/Dynamic/Class.hs
@@ -1,7 +1,43 @@
+{-# LANGUAGE AllowAmbiguousTypes #-}
+{-# LANGUAGE FunctionalDependencies #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
+
+-- |
+-- Module      : Aztecs.ECS.Query.Dynamic.Class
+-- Copyright   : (c) Matt Hunzinger, 2025
+-- License     : BSD-style (see the LICENSE file in the distribution)
+--
+-- Maintainer  : matt@hunzinger.me
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
 module Aztecs.ECS.Query.Dynamic.Class (ArrowDynamicQuery (..)) where
 
 import Aztecs.ECS.Component
-import Aztecs.ECS.Query.Dynamic.Reader.Class (ArrowDynamicQueryReader)
+import Aztecs.ECS.Query.Dynamic.Reader.Class
+import Control.Arrow
 
-class (ArrowDynamicQueryReader arr) => ArrowDynamicQuery arr where
+-- | Arrow dynamic query.
+--
+-- @since 9.0
+class (ArrowDynamicQueryReader arr) => ArrowDynamicQuery m arr | arr -> m where
+  -- | Adjust a `Component` by its `ComponentID`.
+  --
+  -- @since 9.0
+  adjustDyn :: (Component a) => (i -> a -> a) -> ComponentID -> arr i a
+
+  -- | Adjust a `Component` by its `ComponentID`, ignoring any output.
+  --
+  -- @since 9.0
+  adjustDyn_ :: (Component a) => (i -> a -> a) -> ComponentID -> arr i ()
+  adjustDyn_ f cid = adjustDyn @m f cid >>> arr (const ())
+
+  -- | Adjust a `Component` by its `ComponentID` with some `Monad` @m@.
+  --
+  -- @since 9.0
+  adjustDynM :: (Component a) => (i -> a -> m a) -> ComponentID -> arr i a
+
+  -- | Set a `Component` by its `ComponentID`.
+  --
+  -- @since 9.0
   setDyn :: (Component a) => ComponentID -> arr a a
diff --git a/src/Aztecs/ECS/Query/Dynamic/Reader.hs b/src/Aztecs/ECS/Query/Dynamic/Reader.hs
--- a/src/Aztecs/ECS/Query/Dynamic/Reader.hs
+++ b/src/Aztecs/ECS/Query/Dynamic/Reader.hs
@@ -1,19 +1,29 @@
 {-# LANGUAGE BangPatterns #-}
 {-# LANGUAGE DeriveFunctor #-}
-{-# LANGUAGE InstanceSigs #-}
-{-# LANGUAGE RankNTypes #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE TypeApplications #-}
-{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
 
+-- |
+-- Module      : Aztecs.ECS.Query.Dynamic.Reader
+-- Copyright   : (c) Matt Hunzinger, 2025
+-- License     : BSD-style (see the LICENSE file in the distribution)
+--
+-- Maintainer  : matt@hunzinger.me
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
 module Aztecs.ECS.Query.Dynamic.Reader
   ( -- * Dynamic queries
-    DynamicQueryReader (..),
+    DynamicQueryReader,
+    DynamicQueryReaderT (..),
     ArrowDynamicQueryReader (..),
 
     -- ** Running
     allDyn,
+    filterDyn,
+    singleDyn,
+    singleMaybeDyn,
     runDynQueryReader,
+    runDynQueryReaderT,
 
     -- * Dynamic query filters
     DynamicQueryFilter (..),
@@ -21,84 +31,166 @@
 where
 
 import Aztecs.ECS.Component
-import Aztecs.ECS.Entity (EntityID)
-import Aztecs.ECS.Query.Dynamic.Reader.Class (ArrowDynamicQueryReader (..))
+import Aztecs.ECS.Query.Dynamic.Reader.Class
 import Aztecs.ECS.World.Archetype (Archetype)
 import qualified Aztecs.ECS.World.Archetype as A
+import Aztecs.ECS.World.Archetypes (Node)
 import qualified Aztecs.ECS.World.Archetypes as AS
 import Aztecs.ECS.World.Entities (Entities (..))
-import qualified Aztecs.ECS.World.Storage as S
 import Control.Arrow
 import Control.Category
-import Data.Either (partitionEithers)
+import Control.Monad.Identity
+import Data.Either
 import qualified Data.Map as Map
 import Data.Set (Set)
 import qualified Data.Set as Set
+import GHC.Stack
 
+-- | @since 9.0
+type DynamicQueryReader = DynamicQueryReaderT Identity
+
 -- | Dynamic query for components by ID.
-newtype DynamicQueryReader i o
-  = DynamicQueryReader {runDynQueryReader' :: [i] -> [EntityID] -> Archetype -> [o]}
+--
+-- @since 9.0
+newtype DynamicQueryReaderT m i o
+  = DynamicQueryReader
+  { -- | Run a dynamic query with a list of inputs with length equal to the number of entities in the `Archetype`.
+    -- This is an internal function that should typically not be used directly.
+    --
+    -- @since 9.0
+    runDynQueryReader' :: [i] -> Archetype -> m [o]
+  }
   deriving (Functor)
 
-instance Applicative (DynamicQueryReader i) where
-  pure a = DynamicQueryReader $ \_ es _ -> replicate (length es) a
-
+-- | @since 9.0
+instance (Monad m) => Applicative (DynamicQueryReaderT m i) where
+  {-# INLINE pure #-}
+  pure a = DynamicQueryReader $ \is _ -> pure $ replicate (length is) a
+  {-# INLINE (<*>) #-}
   f <*> g =
-    DynamicQueryReader $ \i es arch ->
-      let as = runDynQueryReader' g i es arch
-          fs = runDynQueryReader' f i es arch
-       in zipWith ($) fs as
+    DynamicQueryReader $ \i arch -> do
+      !as <- runDynQueryReader' g i arch
+      !fs <- runDynQueryReader' f i arch
+      return $ zipWith ($) fs as
 
-instance Category DynamicQueryReader where
-  id = DynamicQueryReader $ \as _ _ -> as
-  f . g = DynamicQueryReader $ \i es arch ->
-    let as = runDynQueryReader' g i es arch in runDynQueryReader' f as es arch
+-- | @since 9.0
+instance (Monad m) => Category (DynamicQueryReaderT m) where
+  {-# INLINE id #-}
+  id = DynamicQueryReader $ \as _ -> pure as
+  {-# INLINE (.) #-}
+  f . g = DynamicQueryReader $ \i arch -> do
+    !as <- runDynQueryReader' g i arch
+    runDynQueryReader' f as arch
 
-instance Arrow DynamicQueryReader where
-  arr f = DynamicQueryReader $ \bs _ _ -> fmap f bs
-  first f = DynamicQueryReader $ \bds es arch ->
-    let (bs, ds) = unzip bds
-        cs = runDynQueryReader' f bs es arch
-     in zip cs ds
+-- | @since 9.0
+instance (Monad m) => Arrow (DynamicQueryReaderT m) where
+  {-# INLINE arr #-}
+  arr f = DynamicQueryReader $ \bs _ -> pure $ fmap f bs
+  {-# INLINE first #-}
+  first f = DynamicQueryReader $ \bds arch -> do
+    let !(bs, ds) = unzip bds
+    !cs <- runDynQueryReader' f bs arch
+    return $ zip cs ds
 
-instance ArrowChoice DynamicQueryReader where
-  left f = DynamicQueryReader $ \eds es arch ->
-    let (es', ds) = partitionEithers eds
-        cs = runDynQueryReader' f es' es arch
-     in fmap Left cs ++ fmap Right ds
+-- | @since 9.0
+instance (Monad m) => ArrowChoice (DynamicQueryReaderT m) where
+  {-# INLINE left #-}
+  left f = DynamicQueryReader $ \eds arch -> do
+    let !(es', ds) = partitionEithers eds
+    !cs <- runDynQueryReader' f es' arch
+    return $ fmap Left cs ++ fmap Right ds
 
-instance ArrowDynamicQueryReader DynamicQueryReader where
-  entity = DynamicQueryReader $ \_ es _ -> es
-  fetchDyn :: forall a. (Component a) => ComponentID -> DynamicQueryReader () a
-  fetchDyn cId = DynamicQueryReader $ \_ _ arch ->
-    maybe [] (S.toAscList @a @(StorageT a)) (A.lookupStorage @a cId arch)
-  fetchMaybeDyn :: forall a. (Component a) => ComponentID -> DynamicQueryReader () (Maybe a)
-  fetchMaybeDyn cId = DynamicQueryReader $ \_ es arch -> case A.lookupStorage @a cId arch of
-    Just s -> let !as = S.toAscList @a @(StorageT a) s in fmap Just as
-    Nothing -> map (const Nothing) es
+-- | @since 9.0
+instance (Monad m) => ArrowDynamicQueryReader (DynamicQueryReaderT m) where
+  {-# INLINE entity #-}
+  entity = DynamicQueryReader $ \_ arch -> pure $ Set.toList $ A.entities arch
+  {-# INLINE fetchDyn #-}
+  fetchDyn cId = DynamicQueryReader $ \_ arch -> pure $ A.lookupComponentsAsc cId arch
+  {-# INLINE fetchMaybeDyn #-}
+  fetchMaybeDyn cId = DynamicQueryReader $ \is arch -> pure $ case A.lookupComponentsAscMaybe cId arch of
+    Just as -> fmap Just as
+    Nothing -> map (const Nothing) is
 
+-- | Dynamic query filter.
+--
+-- @since 9.0
 data DynamicQueryFilter = DynamicQueryFilter
-  { filterWith :: !(Set ComponentID),
+  { -- | `ComponentID`s to include.
+    --
+    -- @since 9.0
+    filterWith :: !(Set ComponentID),
+    -- | `ComponentID`s to exclude.
+    --
+    -- @since 9.0
     filterWithout :: !(Set ComponentID)
   }
 
+-- | @since 9.0
 instance Semigroup DynamicQueryFilter where
   DynamicQueryFilter withA withoutA <> DynamicQueryFilter withB withoutB =
     DynamicQueryFilter (withA <> withB) (withoutA <> withoutB)
 
+-- | @since 9.0
 instance Monoid DynamicQueryFilter where
   mempty = DynamicQueryFilter mempty mempty
 
-runDynQueryReader :: i -> DynamicQueryReader i o -> [EntityID] -> Archetype -> [o]
-runDynQueryReader i q = runDynQueryReader' q (repeat i)
+-- | Run a dynamic query.
+--
+-- @since 9.0
+{-# INLINE runDynQueryReaderT #-}
+runDynQueryReaderT :: i -> DynamicQueryReaderT m i o -> Archetype -> m [o]
+runDynQueryReaderT i q arch = runDynQueryReader' q (replicate (length $ A.entities arch) i) arch
 
+-- | Run a dynamic query.
+--
+-- @since 9.0
+{-# INLINE runDynQueryReader #-}
+runDynQueryReader :: i -> DynamicQueryReader i o -> Archetype -> [o]
+runDynQueryReader i q arch = runIdentity $ runDynQueryReaderT i q arch
+
 -- | Match all entities.
-allDyn :: Set ComponentID -> i -> DynamicQueryReader i a -> Entities -> [a]
+--
+-- @since 9.0
+allDyn :: (Monad m) => Set ComponentID -> i -> DynamicQueryReaderT m i a -> Entities -> m [a]
 allDyn cIds i q es =
   if Set.null cIds
-    then runDynQueryReader i q (Map.keys $ entities es) A.empty
+    then runDynQueryReaderT i q A.empty {A.entities = Map.keysSet $ entities es}
     else
-      let go n =
-            let eIds = Set.toList $ A.entities $ AS.nodeArchetype n
-             in runDynQueryReader i q eIds (AS.nodeArchetype n)
-       in concatMap go (AS.find cIds $ archetypes es)
+      let go n = runDynQueryReaderT i q $ AS.nodeArchetype n
+       in concat <$> mapM go (AS.find cIds $ archetypes es)
+
+-- | Match all entities with a filter.
+--
+-- @since 9.0
+filterDyn :: (Monad m) => Set ComponentID -> i -> (Node -> Bool) -> DynamicQueryReaderT m i a -> Entities -> m [a]
+filterDyn cIds i f q es =
+  if Set.null cIds
+    then runDynQueryReaderT i q A.empty {A.entities = Map.keysSet $ entities es}
+    else
+      let go n = runDynQueryReaderT i q $ AS.nodeArchetype n
+       in concat <$> mapM go (Map.filter f $ AS.find cIds $ archetypes es)
+
+-- | Match a single entity.
+--
+-- @since 9.0
+singleDyn :: (HasCallStack) => Set ComponentID -> i -> DynamicQueryReader i a -> Entities -> a
+singleDyn cIds i q es = case singleMaybeDyn cIds i q es of
+  Just a -> a
+  _ -> error "singleDyn: expected a single entity"
+
+-- | Match a single entity, or `Nothing`.
+--
+-- @since 9.0
+singleMaybeDyn :: Set ComponentID -> i -> DynamicQueryReader i a -> Entities -> Maybe a
+singleMaybeDyn cIds i q es =
+  if Set.null cIds
+    then case Map.keys $ entities es of
+      [eId] -> case runDynQueryReader i q $ A.singleton eId of
+        [a] -> Just a
+        _ -> Nothing
+      _ -> Nothing
+    else case Map.elems $ AS.find cIds $ archetypes es of
+      [n] -> case runDynQueryReader i q $ AS.nodeArchetype n of
+        [a] -> Just a
+        _ -> Nothing
+      _ -> Nothing
diff --git a/src/Aztecs/ECS/Query/Dynamic/Reader/Class.hs b/src/Aztecs/ECS/Query/Dynamic/Reader/Class.hs
--- a/src/Aztecs/ECS/Query/Dynamic/Reader/Class.hs
+++ b/src/Aztecs/ECS/Query/Dynamic/Reader/Class.hs
@@ -1,16 +1,33 @@
+-- |
+-- Module      : Aztecs.ECS.Query.Dynamic.Reader.Class
+-- Copyright   : (c) Matt Hunzinger, 2025
+-- License     : BSD-style (see the LICENSE file in the distribution)
+--
+-- Maintainer  : matt@hunzinger.me
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
 module Aztecs.ECS.Query.Dynamic.Reader.Class (ArrowDynamicQueryReader (..)) where
 
 import Aztecs.ECS.Component
-import Aztecs.ECS.Entity (EntityID)
-import Control.Arrow (Arrow (..), (>>>))
+import Aztecs.ECS.Entity
+import Control.Arrow
 
+-- | Arrow dynamic query reader.
+--
+-- @since 9.0
 class (Arrow arr) => ArrowDynamicQueryReader arr where
   -- | Fetch the currently matched `EntityID`.
+  --
+  -- @since 9.0
   entity :: arr () EntityID
 
   -- | Fetch a `Component` by its `ComponentID`.
+  --
+  -- @since 9.0
   fetchDyn :: (Component a) => ComponentID -> arr () a
 
   -- | Try to fetch a `Component` by its `ComponentID`.
+  --
+  -- @since 9.0
   fetchMaybeDyn :: (Component a) => ComponentID -> arr () (Maybe a)
   fetchMaybeDyn cId = fetchDyn cId >>> arr Just
diff --git a/src/Aztecs/ECS/Query/Reader.hs b/src/Aztecs/ECS/Query/Reader.hs
--- a/src/Aztecs/ECS/Query/Reader.hs
+++ b/src/Aztecs/ECS/Query/Reader.hs
@@ -1,20 +1,36 @@
 {-# LANGUAGE AllowAmbiguousTypes #-}
+{-# LANGUAGE BangPatterns #-}
 {-# LANGUAGE DeriveFunctor #-}
+{-# LANGUAGE FlexibleInstances #-}
 {-# LANGUAGE InstanceSigs #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
 {-# LANGUAGE ScopedTypeVariables #-}
 {-# LANGUAGE TupleSections #-}
 {-# LANGUAGE TypeApplications #-}
 {-# LANGUAGE TypeFamilies #-}
 
+-- |
+-- Module      : Aztecs.ECS.Query.Dynamic
+-- Copyright   : (c) Matt Hunzinger, 2025
+-- License     : BSD-style (see the LICENSE file in the distribution)
+--
+-- Maintainer  : matt@hunzinger.me
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
 module Aztecs.ECS.Query.Reader
   ( -- * Queries
-    QueryReader (..),
+    QueryReader,
+    QueryReaderT (..),
     ArrowQueryReader (..),
     ArrowDynamicQueryReader (..),
 
     -- ** Running
     all,
     all',
+    single,
+    single',
+    singleMaybe,
+    singleMaybe',
 
     -- * Filters
     QueryFilter (..),
@@ -25,87 +41,162 @@
 where
 
 import Aztecs.ECS.Component
-import Aztecs.ECS.Query.Dynamic (DynamicQueryFilter (..))
-import Aztecs.ECS.Query.Dynamic.Reader (DynamicQueryReader (..), allDyn)
-import Aztecs.ECS.Query.Dynamic.Reader.Class (ArrowDynamicQueryReader (..))
-import Aztecs.ECS.Query.Reader.Class (ArrowQueryReader (..))
+import Aztecs.ECS.Query.Dynamic.Reader
+import Aztecs.ECS.Query.Reader.Class
 import Aztecs.ECS.World.Components (Components)
 import qualified Aztecs.ECS.World.Components as CS
 import Aztecs.ECS.World.Entities (Entities (..))
 import qualified Aztecs.ECS.World.Entities as E
-import Control.Arrow (Arrow (..), ArrowChoice (..))
-import Control.Category (Category (..))
+import Control.Arrow
+import Control.Category
+import Control.Monad.Identity
 import Data.Set (Set)
 import qualified Data.Set as Set
+import GHC.Stack
 import Prelude hiding (all, id, (.))
 
+-- | @since 9.0
+type QueryReader = QueryReaderT Identity
+
 -- | Query to read from entities.
-newtype QueryReader i o
-  = QueryReader {runQueryReader :: Components -> (Set ComponentID, Components, DynamicQueryReader i o)}
+--
+-- @since 9.0
+newtype QueryReaderT m i o
+  = QueryReader
+  { -- | Run a query reader.
+    --
+    -- @since 9.0
+    runQueryReader :: Components -> (Set ComponentID, Components, DynamicQueryReaderT m i o)
+  }
   deriving (Functor)
 
-instance Applicative (QueryReader i) where
-  pure a = QueryReader $ \cs -> (mempty, cs, pure a)
+-- | @since 9.0
+instance (Monad m) => Applicative (QueryReaderT m i) where
+  {-# INLINE pure #-}
+  pure a = QueryReader (mempty,,pure a)
+  {-# INLINE (<*>) #-}
   (QueryReader f) <*> (QueryReader g) = QueryReader $ \cs ->
-    let (cIdsG, cs', aQS) = g cs
-        (cIdsF, cs'', bQS) = f cs'
+    let !(cIdsG, cs', aQS) = g cs
+        !(cIdsF, cs'', bQS) = f cs'
      in (cIdsG <> cIdsF, cs'', bQS <*> aQS)
 
-instance Category QueryReader where
-  id = QueryReader $ \cs -> (mempty, cs, id)
+-- | @since 9.0
+instance (Monad m) => Category (QueryReaderT m) where
+  {-# INLINE id #-}
+  id = QueryReader (mempty,,id)
+  {-# INLINE (.) #-}
   (QueryReader f) . (QueryReader g) = QueryReader $ \cs ->
-    let (cIdsG, cs', aQS) = g cs
-        (cIdsF, cs'', bQS) = f cs'
+    let !(cIdsG, cs', aQS) = g cs
+        !(cIdsF, cs'', bQS) = f cs'
      in (cIdsG <> cIdsF, cs'', bQS . aQS)
 
-instance Arrow QueryReader where
-  arr f = QueryReader $ \cs -> (mempty, cs, arr f)
-  first (QueryReader f) = QueryReader $ \comps -> let (cIds, comps', qS) = f comps in (cIds, comps', first qS)
+-- | @since 9.0
+instance (Monad m) => Arrow (QueryReaderT m) where
+  {-# INLINE arr #-}
+  arr f = QueryReader (mempty,,arr f)
+  {-# INLINE first #-}
+  first (QueryReader f) = QueryReader $ \cs -> let !(cIds, comps', qS) = f cs in (cIds, comps', first qS)
 
-instance ArrowChoice QueryReader where
-  left (QueryReader f) = QueryReader $ \comps -> let (cIds, comps', qS) = f comps in (cIds, comps', left qS)
+-- | @since 9.0
+instance (Monad m) => ArrowChoice (QueryReaderT m) where
+  {-# INLINE left #-}
+  left (QueryReader f) = QueryReader $ \cs -> let !(cIds, comps', qS) = f cs in (cIds, comps', left qS)
 
-instance ArrowQueryReader QueryReader where
-  fetch :: forall a. (Component a) => QueryReader () a
+-- | @since 9.0
+instance (Monad m) => ArrowQueryReader (QueryReaderT m) where
+  {-# INLINE fetch #-}
+  fetch :: forall a. (Component a) => QueryReaderT m () a
   fetch = QueryReader $ \cs ->
-    let (cId, cs') = CS.insert @a cs in (Set.singleton cId, cs', fetchDyn cId)
-  fetchMaybe :: forall a. (Component a) => QueryReader () (Maybe a)
+    let !(cId, cs') = CS.insert @a cs in (Set.singleton cId, cs', fetchDyn cId)
+
+  {-# INLINE fetchMaybe #-}
+  fetchMaybe :: forall a. (Component a) => QueryReaderT m () (Maybe a)
   fetchMaybe = QueryReader $ \cs ->
-    let (cId, cs') = CS.insert @a cs in (Set.singleton cId, cs', fetchMaybeDyn cId)
+    let !(cId, cs') = CS.insert @a cs in (Set.singleton cId, cs', fetchMaybeDyn cId)
 
-instance ArrowDynamicQueryReader QueryReader where
-  entity = QueryReader $ \cs -> (mempty, cs, entity)
-  fetchDyn cId = QueryReader $ \cs -> (Set.singleton cId, cs, fetchDyn cId)
-  fetchMaybeDyn cId = QueryReader $ \cs -> (Set.singleton cId, cs, fetchMaybeDyn cId)
+-- | @since 9.0
+instance (Monad m) => ArrowDynamicQueryReader (QueryReaderT m) where
+  {-# INLINE entity #-}
+  entity = QueryReader (mempty,,entity)
+  {-# INLINE fetchDyn #-}
+  fetchDyn cId = QueryReader (Set.singleton cId,,fetchDyn cId)
+  {-# INLINE fetchMaybeDyn #-}
+  fetchMaybeDyn cId = QueryReader (Set.singleton cId,,fetchMaybeDyn cId)
 
 -- | Filter for a `Query`.
-newtype QueryFilter = QueryFilter {runQueryFilter :: Components -> (DynamicQueryFilter, Components)}
+--
+-- @since 9.0
+newtype QueryFilter = QueryFilter
+  { -- | Run a query filter.
+    runQueryFilter :: Components -> (DynamicQueryFilter, Components)
+  }
 
+-- | @since 9.0
 instance Semigroup QueryFilter where
   a <> b =
     QueryFilter
       ( \cs ->
-          let (withA', cs') = runQueryFilter a cs
-              (withB', cs'') = runQueryFilter b cs'
+          let !(withA', cs') = runQueryFilter a cs
+              !(withB', cs'') = runQueryFilter b cs'
            in (withA' <> withB', cs'')
       )
 
+-- | @since 9.0
 instance Monoid QueryFilter where
   mempty = QueryFilter (mempty,)
 
 -- | Filter for entities containing this component.
+--
+-- @since 9.0
 with :: forall a. (Component a) => QueryFilter
 with = QueryFilter $ \cs ->
-  let (cId, cs') = CS.insert @a cs in (mempty {filterWith = Set.singleton cId}, cs')
+  let !(cId, cs') = CS.insert @a cs in (mempty {filterWith = Set.singleton cId}, cs')
 
 -- | Filter out entities containing this component.
+--
+-- @since 9.0
 without :: forall a. (Component a) => QueryFilter
 without = QueryFilter $ \cs ->
-  let (cId, cs') = CS.insert @a cs in (mempty {filterWithout = Set.singleton cId}, cs')
+  let !(cId, cs') = CS.insert @a cs in (mempty {filterWithout = Set.singleton cId}, cs')
 
-all :: i -> QueryReader i a -> Entities -> ([a], Entities)
-all i q es = let (as, cs) = all' i q es in (as, es {E.components = cs})
+-- | Match all entities.
+--
+-- @since 9.0
+{-# INLINE all #-}
+all :: (Monad m) => i -> QueryReaderT m i a -> Entities -> (m [a], Entities)
+all i q es = let !(as, cs) = all' i q es in (as, es {E.components = cs})
 
 -- | Match all entities.
-all' :: i -> QueryReader i a -> Entities -> ([a], Components)
-all' i q es = let (rs, cs', dynQ) = runQueryReader q (E.components es) in (allDyn rs i dynQ es, cs')
+--
+-- @since 9.0
+{-# INLINE all' #-}
+all' :: (Monad m) => i -> QueryReaderT m i a -> Entities -> (m [a], Components)
+all' i q es = let !(rs, cs', dynQ) = runQueryReader q (E.components es) in (allDyn rs i dynQ es, cs')
+
+-- | Match a single entity.
+--
+-- @since 9.0
+{-# INLINE single #-}
+single :: (HasCallStack) => i -> QueryReader i a -> Entities -> (a, Entities)
+single i q es = let !(a, cs) = single' i q es in (a, es {E.components = cs})
+
+-- | Match a single entity.
+--
+-- @since 9.0
+{-# INLINE single' #-}
+single' :: (HasCallStack) => i -> QueryReader i a -> Entities -> (a, Components)
+single' i q es = let !(rs, cs', dynQ) = runQueryReader q (E.components es) in (singleDyn rs i dynQ es, cs')
+
+-- | Match a single entity.
+--
+-- @since 9.0
+{-# INLINE singleMaybe #-}
+singleMaybe :: i -> QueryReader i a -> Entities -> (Maybe a, Entities)
+singleMaybe i q es = let !(a, cs) = singleMaybe' i q es in (a, es {E.components = cs})
+
+-- | Match a single entity.
+--
+-- @since 9.0
+{-# INLINE singleMaybe' #-}
+singleMaybe' :: i -> QueryReader i a -> Entities -> (Maybe a, Components)
+singleMaybe' i q es = let !(rs, cs', dynQ) = runQueryReader q (E.components es) in (singleMaybeDyn rs i dynQ es, cs')
diff --git a/src/Aztecs/ECS/Query/Reader/Class.hs b/src/Aztecs/ECS/Query/Reader/Class.hs
--- a/src/Aztecs/ECS/Query/Reader/Class.hs
+++ b/src/Aztecs/ECS/Query/Reader/Class.hs
@@ -1,13 +1,27 @@
+-- |
+-- Module      : Aztecs.ECS.Query.Reader.Class
+-- Copyright   : (c) Matt Hunzinger, 2025
+-- License     : BSD-style (see the LICENSE file in the distribution)
+--
+-- Maintainer  : matt@hunzinger.me
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
 module Aztecs.ECS.Query.Reader.Class (ArrowQueryReader (..)) where
 
 import Aztecs.ECS.Component
-import Control.Arrow (Arrow (..), (>>>))
+import Control.Arrow
 
 -- | Arrow for queries that can read from entities.
+--
+-- @since 9.0
 class (Arrow arr) => ArrowQueryReader arr where
   -- | Fetch a `Component` by its type.
+  --
+  -- @since 9.0
   fetch :: (Component a) => arr () a
 
   -- | Fetch a `Component` by its type, returning `Nothing` if it doesn't exist.
+  --
+  -- @since 9.0
   fetchMaybe :: (Component a) => arr () (Maybe a)
   fetchMaybe = fetch >>> arr Just
diff --git a/src/Aztecs/ECS/Schedule.hs b/src/Aztecs/ECS/Schedule.hs
deleted file mode 100644
--- a/src/Aztecs/ECS/Schedule.hs
+++ /dev/null
@@ -1,121 +0,0 @@
-{-# LANGUAGE DeriveFunctor #-}
-{-# LANGUAGE FlexibleInstances #-}
-{-# LANGUAGE MultiParamTypeClasses #-}
-
-module Aztecs.ECS.Schedule
-  ( -- * Schedules
-    Schedule,
-    ScheduleT (..),
-    ArrowReaderSchedule (..),
-    ArrowSchedule (..),
-    ArrowAccessSchedule (..),
-    fromReaderSchedule,
-    delay,
-    forever,
-    forever_,
-    runSchedule,
-    runSchedule_,
-  )
-where
-
-import Aztecs.ECS.Access (AccessT (..), runAccessT)
-import Aztecs.ECS.Schedule.Access.Class
-import Aztecs.ECS.Schedule.Class
-import Aztecs.ECS.Schedule.Dynamic
-import Aztecs.ECS.Schedule.Reader (ReaderScheduleT (..))
-import Aztecs.ECS.Schedule.Reader.Class
-import Aztecs.ECS.System
-import Aztecs.ECS.System.Dynamic
-import Aztecs.ECS.System.Reader (ReaderSystemT (..))
-import qualified Aztecs.ECS.View as V
-import Aztecs.ECS.World (World (..))
-import qualified Aztecs.ECS.World as W
-import Aztecs.ECS.World.Bundle (Bundle)
-import Aztecs.ECS.World.Components (Components)
-import Aztecs.ECS.World.Entities (Entities (..))
-import Control.Arrow
-import Control.Category
-import Control.DeepSeq
-import Control.Exception
-import Control.Monad.Fix
-import Control.Monad.State (MonadState (..))
-import Control.Monad.Trans (MonadTrans (..))
-import Data.Functor
-import Prelude hiding (id, (.))
-
-type Schedule m = ScheduleT (AccessT m)
-
-accessDyn :: (Monad m) => (i -> m o) -> DynamicScheduleT m i o
-accessDyn f = DynamicSchedule $ \i -> do
-  a <- f i
-  return (a, accessDyn f)
-
-delayDyn :: (Applicative m) => a -> DynamicScheduleT m a a
-delayDyn d = DynamicSchedule $ \this -> pure (d, delayDyn this)
-
--- | System schedule.
-newtype ScheduleT m i o = Schedule {runSchedule' :: Components -> (DynamicScheduleT m i o, Components)}
-  deriving (Functor)
-
-instance (Monad m) => Category (ScheduleT m) where
-  id = Schedule $ \cs -> (id, cs)
-  Schedule f . Schedule g = Schedule $ \cs ->
-    let (g', cs') = g cs
-        (f', cs'') = f cs'
-     in (f' . g', cs'')
-
-instance (Monad m) => Arrow (ScheduleT m) where
-  arr f = Schedule $ \cs -> (arr f, cs)
-  first (Schedule f) = Schedule $ \cs -> let (f', cs') = f cs in (first f', cs')
-
-instance (MonadFix m) => ArrowLoop (ScheduleT m) where
-  loop (Schedule f) = Schedule $ \cs -> let (f', cs') = f cs in (loop f', cs')
-
-instance (Monad m) => ArrowAccessSchedule Bundle (AccessT m) (Schedule m) where
-  access f = Schedule $ \cs -> (accessDyn f, cs)
-
-instance (Monad m) => ArrowReaderSchedule (ReaderSystemT m) (Schedule m) where
-  reader = fromReaderSchedule . reader
-
-instance (Monad m) => ArrowSchedule (SystemT m) (Schedule m) where
-  system s = Schedule $ \cs ->
-    let (dynS, _, cs') = runSystem s cs
-        go dynSAcc i = AccessT $ do
-          w <- get
-          let (o, v, a, dynSAcc') = runSystemDyn dynSAcc (W.entities w) i
-          ((), w') <- lift $ runAccessT a w {W.entities = V.unview v (W.entities w)}
-          put w'
-          return (o, DynamicSchedule $ go dynSAcc')
-     in (DynamicSchedule $ go dynS, cs')
-
-fromReaderSchedule :: (Monad m) => ReaderScheduleT m i o -> ScheduleT m i o
-fromReaderSchedule s = Schedule $ \cs ->
-  let (dynS, cs') = runReaderSchedule s cs in (fromDynReaderSchedule dynS, cs')
-
-delay :: (Monad m) => a -> Schedule m a a
-delay d = Schedule $ \cs -> (delayDyn d, cs)
-
-runSchedule :: (Monad m) => Schedule m i o -> World -> i -> m (o, DynamicSchedule m i o, World)
-runSchedule s w i = do
-  let (f, cs) = runSchedule' s (components $ W.entities w)
-  ((o, f'), w') <- runAccessT (runScheduleDyn f i) w {W.entities = (W.entities w) {components = cs}}
-  return (o, f', w')
-
-runSchedule_ :: (Monad m) => Schedule m () () -> m ()
-runSchedule_ s = void (runSchedule s W.empty ())
-
-forever :: Schedule IO i o -> (o -> IO ()) -> Schedule IO i ()
-forever s f = Schedule $ \cs ->
-  let (g, cs') = runSchedule' s cs
-      go i = AccessT $ do
-        w <- get
-        let go' gAcc wAcc = do
-              ((o, g'), wAcc') <- lift $ runAccessT (runScheduleDyn gAcc i) wAcc
-              lift $ evaluate $ rnf wAcc'
-              lift $ f o
-              go' g' wAcc'
-        go' g w
-   in (DynamicSchedule go, cs')
-
-forever_ :: Schedule IO i o -> Schedule IO i ()
-forever_ s = forever s (const $ pure ())
diff --git a/src/Aztecs/ECS/Schedule/Access.hs b/src/Aztecs/ECS/Schedule/Access.hs
deleted file mode 100644
--- a/src/Aztecs/ECS/Schedule/Access.hs
+++ /dev/null
@@ -1,27 +0,0 @@
-{-# LANGUAGE DeriveFunctor #-}
-{-# LANGUAGE MultiParamTypeClasses #-}
-
-module Aztecs.ECS.Schedule.Access (AcessSchedule (..), ArrowAccessSchedule (..)) where
-
-import Aztecs.ECS.Access (AccessT (..))
-import Aztecs.ECS.Schedule (ArrowAccessSchedule (..))
-import Aztecs.ECS.World.Bundle (Bundle)
-import Control.Arrow (Arrow (..))
-import Control.Category (Category (..))
-import Control.Monad ((>=>))
-
-newtype AcessSchedule m i o = AcessSchedule {runAcessSchedule :: i -> AccessT m o}
-  deriving (Functor)
-
-instance (Monad m) => Category (AcessSchedule m) where
-  id = AcessSchedule return
-  AcessSchedule f . AcessSchedule g = AcessSchedule (g >=> f)
-
-instance (Monad m) => Arrow (AcessSchedule m) where
-  arr f = AcessSchedule $ \i -> return $ f i
-  first (AcessSchedule f) = AcessSchedule $ \(b, d) -> do
-    c <- f b
-    return (c, d)
-
-instance (Monad m) => ArrowAccessSchedule Bundle (AccessT m) (AcessSchedule m) where
-  access = AcessSchedule
diff --git a/src/Aztecs/ECS/Schedule/Access/Class.hs b/src/Aztecs/ECS/Schedule/Access/Class.hs
deleted file mode 100644
--- a/src/Aztecs/ECS/Schedule/Access/Class.hs
+++ /dev/null
@@ -1,11 +0,0 @@
-{-# LANGUAGE FunctionalDependencies #-}
-
-module Aztecs.ECS.Schedule.Access.Class (ArrowAccessSchedule (..)) where
-
-import Aztecs.ECS.Access (MonadAccess)
-import Control.Arrow (Arrow (..))
-
--- | Schedule arrow that provides access to a `World`.
-class (MonadAccess b m, Arrow arr) => ArrowAccessSchedule b m arr | arr -> m where
-  -- | Access the `World`.
-  access :: (i -> m o) -> arr i o
diff --git a/src/Aztecs/ECS/Schedule/Class.hs b/src/Aztecs/ECS/Schedule/Class.hs
deleted file mode 100644
--- a/src/Aztecs/ECS/Schedule/Class.hs
+++ /dev/null
@@ -1,10 +0,0 @@
-{-# LANGUAGE FunctionalDependencies #-}
-
-module Aztecs.ECS.Schedule.Class (ArrowSchedule (..)) where
-
-import Control.Arrow (Arrow (..))
-
--- | Schedule arrow that runs systems.
-class (Arrow arr) => ArrowSchedule s arr | arr -> s where
-  -- | Schedule a system.
-  system :: s i o -> arr i o
diff --git a/src/Aztecs/ECS/Schedule/Dynamic.hs b/src/Aztecs/ECS/Schedule/Dynamic.hs
deleted file mode 100644
--- a/src/Aztecs/ECS/Schedule/Dynamic.hs
+++ /dev/null
@@ -1,51 +0,0 @@
-{-# LANGUAGE DeriveFunctor #-}
-{-# LANGUAGE RecursiveDo #-}
-
-module Aztecs.ECS.Schedule.Dynamic
-  ( DynamicSchedule,
-    DynamicScheduleT (..),
-    fromDynReaderSchedule,
-  )
-where
-
-import Aztecs.ECS.Access
-import Aztecs.ECS.Schedule.Dynamic.Reader (DynamicReaderScheduleT (..))
-import Control.Arrow
-import Control.Category
-import Control.Monad.Fix
-import Prelude hiding (id, (.))
-
-type DynamicSchedule m = DynamicScheduleT (AccessT m)
-
-newtype DynamicScheduleT m i o = DynamicSchedule {runScheduleDyn :: i -> m (o, DynamicScheduleT m i o)}
-  deriving (Functor)
-
-instance (Monad m) => Category (DynamicScheduleT m) where
-  id = DynamicSchedule $ \i -> pure (i, id)
-  DynamicSchedule f . DynamicSchedule g = DynamicSchedule $ \i -> do
-    (b, g') <- g i
-    (c, f') <- f b
-    return (c, f' . g')
-
-instance (Monad m) => Arrow (DynamicScheduleT m) where
-  arr f = DynamicSchedule $ \i -> pure (f i, arr f)
-  first (DynamicSchedule f) = DynamicSchedule $ \(b, d) -> do
-    (c, f') <- f b
-    return ((c, d), first f')
-
-instance (Monad m) => ArrowChoice (DynamicScheduleT m) where
-  left (DynamicSchedule f) = DynamicSchedule $ \i -> case i of
-    Left b -> do
-      (c, f') <- f b
-      return (Left c, left f')
-    Right d -> return (Right d, left (DynamicSchedule f))
-
-instance (MonadFix m) => ArrowLoop (DynamicScheduleT m) where
-  loop (DynamicSchedule f) = DynamicSchedule $ \b -> do
-    rec ((c, d), f') <- f (b, d)
-    return (c, loop f')
-
-fromDynReaderSchedule :: (Monad m) => DynamicReaderScheduleT m i o -> DynamicScheduleT m i o
-fromDynReaderSchedule (DynamicReaderSchedule f) = DynamicSchedule $ \i -> do
-  (o, f') <- f i
-  return (o, fromDynReaderSchedule f')
diff --git a/src/Aztecs/ECS/Schedule/Dynamic/Reader.hs b/src/Aztecs/ECS/Schedule/Dynamic/Reader.hs
deleted file mode 100644
--- a/src/Aztecs/ECS/Schedule/Dynamic/Reader.hs
+++ /dev/null
@@ -1,45 +0,0 @@
-{-# LANGUAGE DeriveFunctor #-}
-{-# LANGUAGE RecursiveDo #-}
-{-# LANGUAGE TypeFamilies #-}
-
-module Aztecs.ECS.Schedule.Dynamic.Reader
-  ( DynamicReaderSchedule,
-    DynamicReaderScheduleT (..),
-  )
-where
-
-import Aztecs.ECS.Access
-import Control.Arrow
-import Control.Category
-import Control.Monad.Fix
-import Prelude hiding (all, any, id, lookup, reads, (.))
-
-type DynamicReaderSchedule m = DynamicReaderScheduleT (AccessT m)
-
-newtype DynamicReaderScheduleT m i o = DynamicReaderSchedule {runScheduleDyn :: i -> m (o, DynamicReaderScheduleT m i o)}
-  deriving (Functor)
-
-instance (Monad m) => Category (DynamicReaderScheduleT m) where
-  id = DynamicReaderSchedule $ \i -> pure (i, id)
-  DynamicReaderSchedule f . DynamicReaderSchedule g = DynamicReaderSchedule $ \i -> do
-    (b, g') <- g i
-    (c, f') <- f b
-    return (c, f' . g')
-
-instance (Monad m) => Arrow (DynamicReaderScheduleT m) where
-  arr f = DynamicReaderSchedule $ \i -> pure (f i, arr f)
-  first (DynamicReaderSchedule f) = DynamicReaderSchedule $ \(b, d) -> do
-    (c, f') <- f b
-    return ((c, d), first f')
-
-instance (Monad m) => ArrowChoice (DynamicReaderScheduleT m) where
-  left (DynamicReaderSchedule f) = DynamicReaderSchedule $ \i -> case i of
-    Left b -> do
-      (c, f') <- f b
-      return (Left c, left f')
-    Right d -> return (Right d, left (DynamicReaderSchedule f))
-
-instance (MonadFix m) => ArrowLoop (DynamicReaderScheduleT m) where
-  loop (DynamicReaderSchedule f) = DynamicReaderSchedule $ \b -> do
-    rec ((c, d), f') <- f (b, d)
-    return (c, loop f')
diff --git a/src/Aztecs/ECS/Schedule/Reader.hs b/src/Aztecs/ECS/Schedule/Reader.hs
deleted file mode 100644
--- a/src/Aztecs/ECS/Schedule/Reader.hs
+++ /dev/null
@@ -1,56 +0,0 @@
-{-# LANGUAGE DeriveFunctor #-}
-{-# LANGUAGE FlexibleInstances #-}
-{-# LANGUAGE MultiParamTypeClasses #-}
-
-module Aztecs.ECS.Schedule.Reader
-  ( ReaderScheduleT (..),
-  )
-where
-
-import Aztecs.ECS.Access
-import Aztecs.ECS.Schedule.Dynamic.Reader
-import Aztecs.ECS.Schedule.Reader.Class
-import Aztecs.ECS.System.Dynamic.Reader
-import Aztecs.ECS.System.Reader
-import Aztecs.ECS.World (World (..))
-import Aztecs.ECS.World.Components (Components)
-import Control.Arrow
-import Control.Category
-import Control.Monad.Fix
-import Control.Monad.State (MonadState (..))
-import Control.Monad.Trans
-import Prelude hiding (id, (.))
-
-type ReaderSchedule m = ReaderScheduleT (AccessT m)
-
-newtype ReaderScheduleT m i o
-  = ReaderSchedule {runReaderSchedule :: Components -> (DynamicReaderScheduleT m i o, Components)}
-  deriving (Functor)
-
-instance (Monad m) => Category (ReaderScheduleT m) where
-  id = ReaderSchedule $ \cs -> (id, cs)
-  ReaderSchedule f . ReaderSchedule g = ReaderSchedule $ \cs ->
-    let (g', cs') = g cs
-        (f', cs'') = f cs'
-     in (f' . g', cs'')
-
-instance (Monad m) => Arrow (ReaderScheduleT m) where
-  arr f = ReaderSchedule $ \cs -> (arr f, cs)
-  first (ReaderSchedule f) = ReaderSchedule $ \cs -> let (f', cs') = f cs in (first f', cs')
-
-instance (Monad m) => ArrowChoice (ReaderScheduleT m) where
-  left (ReaderSchedule f) = ReaderSchedule $ \cs -> let (f', cs') = f cs in (left f', cs')
-
-instance (MonadFix m) => ArrowLoop (ReaderScheduleT m) where
-  loop (ReaderSchedule f) = ReaderSchedule $ \cs -> let (f', cs') = f cs in (loop f', cs')
-
-instance (Monad m) => ArrowReaderSchedule (ReaderSystemT m) (ReaderSchedule m) where
-  reader s = ReaderSchedule $ \cs ->
-    let (dynS, _, cs') = runReaderSystem s cs
-        go dynSAcc i = AccessT $ do
-          w <- get
-          let (o, a, dynSAcc') = runReaderSystemDyn dynSAcc (entities w) i
-          ((), w') <- lift $ runAccessT a w
-          put w'
-          return (o, DynamicReaderSchedule $ go dynSAcc')
-     in (DynamicReaderSchedule $ go dynS, cs')
diff --git a/src/Aztecs/ECS/Schedule/Reader/Class.hs b/src/Aztecs/ECS/Schedule/Reader/Class.hs
deleted file mode 100644
--- a/src/Aztecs/ECS/Schedule/Reader/Class.hs
+++ /dev/null
@@ -1,10 +0,0 @@
-{-# LANGUAGE FunctionalDependencies #-}
-
-module Aztecs.ECS.Schedule.Reader.Class (ArrowReaderSchedule (..)) where
-
-import Control.Arrow (Arrow (..))
-
--- | Schedule arrow that runs read-only systems.
-class (Arrow arr) => ArrowReaderSchedule s arr | arr -> s where
-  -- | Schedule a reader system.
-  reader :: s i o -> arr i o
diff --git a/src/Aztecs/ECS/System.hs b/src/Aztecs/ECS/System.hs
--- a/src/Aztecs/ECS/System.hs
+++ b/src/Aztecs/ECS/System.hs
@@ -1,93 +1,163 @@
 {-# LANGUAGE BangPatterns #-}
 {-# LANGUAGE DeriveFunctor #-}
 {-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
 {-# LANGUAGE MultiParamTypeClasses #-}
 
+-- |
+-- Module      : Aztecs.ECS.System
+-- Copyright   : (c) Matt Hunzinger, 2025
+-- License     : BSD-style (see the LICENSE file in the distribution)
+--
+-- Maintainer  : matt@hunzinger.me
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
+--
+-- Systems to process queries in parallel.
 module Aztecs.ECS.System
   ( System,
     SystemT (..),
-    ArrowReaderSystem (..),
-    ArrowSystem (..),
-    ArrowQueueSystem (..),
-    fromReader,
+    MonadReaderSystem (..),
+    MonadSystem (..),
+    MonadDynamicReaderSystem (..),
+    MonadDynamicSystem (..),
   )
 where
 
-import Aztecs.ECS.Access
-import Aztecs.ECS.Query (Query (..), QueryFilter (..), ReadsWrites (..))
+import Aztecs.ECS.Component
+import Aztecs.ECS.Query
 import qualified Aztecs.ECS.Query as Q
-import Aztecs.ECS.Query.Reader (DynamicQueryFilter (..), QueryReader (..))
+import Aztecs.ECS.Query.Dynamic (DynamicQueryT)
+import Aztecs.ECS.Query.Dynamic.Reader (DynamicQueryReaderT, runDynQueryReaderT)
+import Aztecs.ECS.Query.Reader
 import Aztecs.ECS.System.Class
-import Aztecs.ECS.System.Dynamic
-import Aztecs.ECS.System.Reader
+import Aztecs.ECS.System.Dynamic.Class
+import Aztecs.ECS.System.Dynamic.Reader.Class
+import Aztecs.ECS.System.Reader.Class
+import qualified Aztecs.ECS.View as V
 import qualified Aztecs.ECS.World.Archetype as A
 import Aztecs.ECS.World.Archetypes (Node (..))
-import Aztecs.ECS.World.Bundle
-import Aztecs.ECS.World.Components (Components)
-import Control.Arrow
+import Aztecs.ECS.World.Entities (Entities (..))
 import Control.Category
+import Control.Concurrent.STM
 import Control.Monad.Identity
+import Control.Monad.Reader
 import qualified Data.Foldable as F
+import qualified Data.Map as Map
+import Data.Set (Set)
 import Prelude hiding (all, filter, id, map, (.))
-import qualified Prelude hiding (filter, id, map)
 
-type System = SystemT Identity
+-- | @since 9.0
+type System = SystemT STM
 
--- | System to process entities.
-newtype SystemT m i o = System
-  { -- | Run a system, producing a `DynamicSystem` that can be repeatedly run.
-    runSystem :: Components -> (DynamicSystemT m i o, ReadsWrites, Components)
+-- | System to process queries in parallel.
+--
+-- @since 9.0
+newtype SystemT m a = SystemT
+  { -- | Run a system on a collection of `Entities`.
+    --
+    -- @since 9.0
+    runSystemT :: ReaderT (TVar Entities) m a
   }
-  deriving (Functor)
-
-instance (Monad m) => Category (SystemT m) where
-  id = System $ \cs -> (DynamicSystem $ \_ i -> (i, mempty, pure (), id), mempty, cs)
-  System f . System g = System $ \cs ->
-    let (f', rwsF, cs') = f cs
-        (g', rwsG, cs'') = g cs'
-     in (f' . g', rwsF <> rwsG, cs'')
-
-instance (Monad m) => Arrow (SystemT m) where
-  arr f = System $ \cs -> (DynamicSystem $ \_ i -> (f i, mempty, pure (), arr f), mempty, cs)
-  first (System f) = System $ \cs ->
-    let (f', rwsF, cs') = f cs in (first f', rwsF, cs')
-  f &&& g = System $ \cs ->
-    let (dynF, rwsA, cs') = runSystem f cs
-        (dynG, rwsB, cs'') = runSystem g cs'
-        dynS = if Q.disjoint rwsA rwsB then dynF &&& dynG else raceDyn dynF dynG
-     in (dynS, rwsA <> rwsB, cs'')
+  deriving (Functor, Applicative, Monad, MonadFix)
 
-instance (Monad m) => ArrowChoice (SystemT m) where
-  left (System f) = System $ \cs -> let (f', rwsF, cs') = f cs in (left f', rwsF, cs')
+-- | @since 9.0
+instance MonadDynamicSystem (DynamicQueryT STM) System where
+  mapDyn i cIds q = SystemT $ do
+    wVar <- ask
+    w <- lift $ readTVar wVar
+    let !v = V.view cIds $ archetypes w
+    (o, v') <- lift $ V.mapDyn i q v
+    lift . modifyTVar wVar $ V.unview v'
+    return o
+  mapSingleMaybeDyn i cIds q = SystemT $ do
+    wVar <- ask
+    w <- lift $ readTVar wVar
+    case V.viewSingle cIds $ archetypes w of
+      Just v -> do
+        (o, v') <- lift $ V.mapSingleDyn i q v
+        lift . modifyTVar wVar $ V.unview v'
+        return o
+      Nothing -> return Nothing
+  filterMapDyn i cIds f q = SystemT $ do
+    wVar <- ask
+    w <- lift $ readTVar wVar
+    let !v = V.filterView cIds f $ archetypes w
+    (o, v') <- lift $ V.mapDyn i q v
+    lift . modifyTVar wVar $ V.unview v'
+    return o
 
-instance (Monad m) => ArrowLoop (SystemT m) where
-  loop (System f) = System $ \cs -> let (f', rwsF, cs') = f cs in (loop f', rwsF, cs')
+-- | @since 9.0
+instance MonadSystem (QueryT STM) System where
+  map i q = SystemT $ do
+    (rws, dynQ) <- runSystemT $ fromQuery q
+    runSystemT $ mapDyn i (Q.reads rws <> Q.writes rws) dynQ
+  mapSingleMaybe i q = SystemT $ do
+    (rws, dynQ) <- runSystemT $ fromQuery q
+    runSystemT $ mapSingleMaybeDyn i (Q.reads rws <> Q.writes rws) dynQ
+  filterMap i q qf = SystemT $ do
+    wVar <- ask
+    let go w =
+          let (rws, cs', dynQ) = runQuery q $ components w
+              (dynF, cs'') = runQueryFilter qf cs'
+           in ((rws, dynQ, dynF), w {components = cs''})
+    (rws, dynQ, dynF) <- lift $ stateTVar wVar go
+    let f' n =
+          F.all (\cId -> A.member cId $ nodeArchetype n) (filterWith dynF)
+            && F.all (\cId -> not (A.member cId $ nodeArchetype n)) (filterWithout dynF)
+    runSystemT $ filterMapDyn i (Q.reads rws <> Q.writes rws) f' dynQ
 
-instance (Monad m) => ArrowReaderSystem QueryReader (SystemT m) where
-  all = fromReader . all
-  filter q = fromReader . filter q
+-- | @since 9.0
+instance MonadReaderSystem (QueryReaderT STM) System where
+  all i q = SystemT $ do
+    (cIds, dynQ) <- runSystemT $ fromQueryReader q
+    runSystemT $ allDyn i cIds dynQ
+  filter i q qf = SystemT $ do
+    wVar <- ask
+    let go w =
+          let (cIds, cs', dynQ) = runQueryReader q $ components w
+              (dynF, cs'') = runQueryFilter qf cs'
+           in ((cIds, dynQ, dynF), w {components = cs''})
+    (cIds, dynQ, dynF) <- lift $ stateTVar wVar go
+    let f' n =
+          F.all (\cId -> A.member cId $ nodeArchetype n) (filterWith dynF)
+            && F.all (\cId -> not (A.member cId $ nodeArchetype n)) (filterWithout dynF)
+    runSystemT $ filterDyn i cIds dynQ f'
 
-instance (Monad m) => ArrowSystem Query (SystemT m) where
-  map q = System $ \cs ->
-    let !(rws, cs', dynQ) = runQuery q cs
-     in (mapDyn (Q.reads rws <> Q.writes rws) dynQ, rws, cs')
-  filterMap q qf = System $ \cs ->
-    let !(rws, cs', dynQ) = runQuery q cs
-        !(dynQf, cs'') = runQueryFilter qf cs'
-        f' n =
-          F.all (\cId -> A.member cId $ nodeArchetype n) (filterWith dynQf)
-            && F.all (\cId -> not (A.member cId $ nodeArchetype n)) (filterWithout dynQf)
-     in (filterMapDyn (Q.reads rws <> Q.writes rws) dynQ f', rws, cs'')
-  mapSingle q = System $ \cs ->
-    let !(rws, cs', dynQ) = runQuery q cs
-     in (mapSingleDyn (Q.reads rws <> Q.writes rws) dynQ, rws, cs')
-  mapSingleMaybe q = System $ \cs ->
-    let !(rws, cs', dynQ) = runQuery q cs
-     in (mapSingleMaybeDyn (Q.reads rws <> Q.writes rws) dynQ, rws, cs')
+-- | @since 9.0
+instance MonadDynamicReaderSystem (DynamicQueryReaderT STM) System where
+  allDyn i cIds q = SystemT $ do
+    wVar <- ask
+    w <- lift $ readTVar wVar
+    let !v = V.view cIds $ archetypes w
+    lift $
+      if V.null v
+        then runDynQueryReaderT i q A.empty {A.entities = Map.keysSet $ entities w}
+        else V.allDyn i q v
+  filterDyn i cIds q f = SystemT $ do
+    wVar <- ask
+    w <- lift $ readTVar wVar
+    let !v = V.filterView cIds f $ archetypes w
+    lift $ V.allDyn i q v
 
-instance (Monad m) => ArrowQueueSystem Bundle (AccessT m) (SystemT m) where
-  queue f = System $ \cs -> (queue f, mempty, cs)
+-- | Convert a `QueryReaderT` to a `System`.
+--
+-- @since 9.0
+fromQueryReader :: QueryReaderT STM i o -> System (Set ComponentID, DynamicQueryReaderT STM i o)
+fromQueryReader q = SystemT $ do
+  wVar <- ask
+  let go w =
+        let (cIds, cs', dynQ) = runQueryReader q $ components w
+         in ((cIds, dynQ), w {components = cs'})
+  lift $ stateTVar wVar go
 
-fromReader :: (Monad m) => ReaderSystemT m i o -> SystemT m i o
-fromReader (ReaderSystem f) = System $ \cs ->
-  let (f', rs, cs') = f cs in (fromDynReaderSystem f', ReadsWrites rs mempty, cs')
+-- | Convert a `QueryT` to a `System`.
+--
+-- @since 9.0
+fromQuery :: QueryT STM i o -> System (ReadsWrites, DynamicQueryT STM i o)
+fromQuery q = SystemT $ do
+  wVar <- ask
+  let go w =
+        let (rws, cs', dynQ) = runQuery q $ components w
+         in ((rws, dynQ), w {components = cs'})
+  lift $ stateTVar wVar go
diff --git a/src/Aztecs/ECS/System/Class.hs b/src/Aztecs/ECS/System/Class.hs
--- a/src/Aztecs/ECS/System/Class.hs
+++ b/src/Aztecs/ECS/System/Class.hs
@@ -1,24 +1,44 @@
 {-# LANGUAGE FunctionalDependencies #-}
 
-module Aztecs.ECS.System.Class (ArrowSystem (..)) where
+-- |
+-- Module      : Aztecs.ECS.System.Class
+-- Copyright   : (c) Matt Hunzinger, 2025
+-- License     : BSD-style (see the LICENSE file in the distribution)
+--
+-- Maintainer  : matt@hunzinger.me
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
+module Aztecs.ECS.System.Class (MonadSystem (..)) where
 
 import Aztecs.ECS.Query.Reader (QueryFilter (..))
-import Control.Arrow (Arrow (..), (>>>))
+import GHC.Stack
 import Prelude hiding (map)
 
-class (Arrow arr) => ArrowSystem q arr | arr -> q where
-  -- | Query and update all matching entities.
-  map :: q i a -> arr i [a]
-
-  -- | Query and update all matching entities, ignoring the results.
-  map_ :: q i o -> arr i ()
-  map_ q = map q >>> arr (const ())
+-- | Monadic system.
+--
+-- @since 9.0
+class (Monad m) => MonadSystem q m | m -> q where
+  -- | Map all matching entities with a query.
+  --
+  -- @since 9.0
+  map :: i -> q i o -> m [o]
 
-  -- | Map all matching entities with a `QueryFilter`, storing the updated entities.
-  filterMap :: q i a -> QueryFilter -> arr i [a]
+  -- | Map a single matching entity with a query, or @Nothing@.
+  --
+  -- @since 9.0
+  mapSingleMaybe :: i -> q i o -> m (Maybe o)
 
-  -- | Map a single matching entity, storing the updated components.
-  -- If there are zero or multiple matching entities, an error will be thrown.
-  mapSingle :: q i a -> arr i a
+  -- | Map a single matching entity with a query.
+  --
+  -- @since 9.0
+  mapSingle :: (HasCallStack) => i -> q i o -> m o
+  mapSingle i q = do
+    res <- mapSingleMaybe i q
+    case res of
+      Just a -> return a
+      Nothing -> error "Expected a single matching entity."
 
-  mapSingleMaybe :: q i a -> arr i (Maybe a)
+  -- | Map all matching entities with a query and filter.
+  --
+  -- @since 9.0
+  filterMap :: i -> q i o -> QueryFilter -> m [o]
diff --git a/src/Aztecs/ECS/System/Dynamic.hs b/src/Aztecs/ECS/System/Dynamic.hs
deleted file mode 100644
--- a/src/Aztecs/ECS/System/Dynamic.hs
+++ /dev/null
@@ -1,104 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DeriveFunctor #-}
-{-# LANGUAGE FlexibleInstances #-}
-{-# LANGUAGE MultiParamTypeClasses #-}
-
-module Aztecs.ECS.System.Dynamic
-  ( DynamicSystem,
-    DynamicSystemT (..),
-    ArrowDynamicReaderSystem (..),
-    ArrowDynamicSystem (..),
-    ArrowQueueSystem (..),
-    raceDyn,
-    fromDynReaderSystem,
-  )
-where
-
-import Aztecs.ECS.Access
-import Aztecs.ECS.Query.Dynamic (DynamicQuery (..))
-import Aztecs.ECS.Query.Dynamic.Reader (DynamicQueryReader (..))
-import Aztecs.ECS.System.Dynamic.Class
-import Aztecs.ECS.System.Dynamic.Reader (DynamicReaderSystemT (..))
-import Aztecs.ECS.System.Dynamic.Reader.Class
-import Aztecs.ECS.System.Queue (ArrowQueueSystem (..))
-import Aztecs.ECS.View (View)
-import qualified Aztecs.ECS.View as V
-import Aztecs.ECS.World.Bundle
-import Aztecs.ECS.World.Entities (Entities (..))
-import Control.Arrow
-import Control.Category
-import Control.Monad.Identity
-import Control.Parallel (par)
-import Data.Maybe (fromMaybe)
-import Prelude hiding (id, (.))
-
-type DynamicSystem = DynamicSystemT Identity
-
-newtype DynamicSystemT m i o = DynamicSystem
-  { -- | Run a dynamic system,
-    -- producing some output, an updated `View` into the `World`, and any queued `Access`.
-    runSystemDyn :: Entities -> i -> (o, View, AccessT m (), DynamicSystemT m i o)
-  }
-  deriving (Functor)
-
-instance (Monad m) => Category (DynamicSystemT m) where
-  id = DynamicSystem $ \_ i -> (i, mempty, pure (), id)
-  DynamicSystem f . DynamicSystem g = DynamicSystem $ \w i ->
-    let (b, gView, gAccess, g') = g w i
-        (a, fView, fAccess, f') = f w b
-     in (a, gView <> fView, gAccess >> fAccess, f' . g')
-
-instance (Monad m) => Arrow (DynamicSystemT m) where
-  arr f = DynamicSystem $ \_ i -> (f i, mempty, pure (), arr f)
-  first (DynamicSystem f) = DynamicSystem $ \w (i, x) ->
-    let (a, v, access, f') = f w i in ((a, x), v, access, first f')
-
-instance (Monad m) => ArrowChoice (DynamicSystemT m) where
-  left (DynamicSystem f) = DynamicSystem $ \w i -> case i of
-    Left b -> let (c, v, access, f') = f w b in (Left c, v, access, left f')
-    Right d -> (Right d, mempty, pure (), left (DynamicSystem f))
-
-instance (Monad m) => ArrowLoop (DynamicSystemT m) where
-  loop (DynamicSystem f) = DynamicSystem $ \w b ->
-    let ((c, d), v, access, f') = f w (b, d) in (c, v, access, loop f')
-
-instance (Monad m) => ArrowDynamicReaderSystem DynamicQueryReader (DynamicSystemT m) where
-  allDyn cIds q = fromDynReaderSystem $ allDyn cIds q
-  filterDyn cIds qf q = fromDynReaderSystem $ filterDyn cIds qf q
-
-instance (Monad m) => ArrowDynamicSystem DynamicQuery (DynamicSystemT m) where
-  mapDyn cIds q = DynamicSystem $ \w i ->
-    let !v = V.view cIds $ archetypes w
-        (o, v') = V.allDyn i q v
-     in (o, v', pure (), mapDyn cIds q)
-  mapSingleDyn cIds q = DynamicSystem $ \w i ->
-    let s =
-          mapSingleMaybeDyn cIds q
-            >>> arr (fromMaybe (error "Expected a single matching entity."))
-     in runSystemDyn s w i
-  mapSingleMaybeDyn cIds q = DynamicSystem $ \w i ->
-    let !res = V.viewSingle cIds $ archetypes w
-        (res', v'') = case res of
-          Just v -> let (o, v') = V.singleDyn i q v in (o, v')
-          Nothing -> (Nothing, mempty)
-     in (res', v'', pure (), mapSingleMaybeDyn cIds q)
-  filterMapDyn cIds q f = DynamicSystem $ \w i ->
-    let !v = V.filterView cIds f $ archetypes w
-        (o, v') = V.allDyn i q v
-     in (o, v', pure (), filterMapDyn cIds q f)
-
-instance (Monad m) => ArrowQueueSystem Bundle (AccessT m) (DynamicSystemT m) where
-  queue f = DynamicSystem $ \_ i -> ((), mempty, f i, queue f)
-
-raceDyn :: (Monad m) => DynamicSystemT m i a -> DynamicSystemT m i b -> DynamicSystemT m i (a, b)
-raceDyn (DynamicSystem f) (DynamicSystem g) = DynamicSystem $ \w i ->
-  let fa = f w i
-      gb = g w i
-      gbPar = fa `par` gb
-      (a, v, fAccess, f') = fa
-      (b, v', gAccess, g') = gbPar
-   in ((a, b), v <> v', fAccess >> gAccess, raceDyn f' g')
-
-fromDynReaderSystem :: DynamicReaderSystemT m i o -> DynamicSystemT m i o
-fromDynReaderSystem (DynamicReaderSystem f) = DynamicSystem $ \w i ->
-  let (o, access, f') = f w i in (o, mempty, access, fromDynReaderSystem f')
diff --git a/src/Aztecs/ECS/System/Dynamic/Class.hs b/src/Aztecs/ECS/System/Dynamic/Class.hs
--- a/src/Aztecs/ECS/System/Dynamic/Class.hs
+++ b/src/Aztecs/ECS/System/Dynamic/Class.hs
@@ -1,21 +1,43 @@
 {-# LANGUAGE FunctionalDependencies #-}
 
-module Aztecs.ECS.System.Dynamic.Class (ArrowDynamicSystem (..)) where
+-- |
+-- Module      : Aztecs.ECS.System.Dynamic.Class
+-- Copyright   : (c) Matt Hunzinger, 2025
+-- License     : BSD-style (see the LICENSE file in the distribution)
+--
+-- Maintainer  : matt@hunzinger.me
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
+module Aztecs.ECS.System.Dynamic.Class (MonadDynamicSystem (..)) where
 
 import Aztecs.ECS.Component (ComponentID)
 import Aztecs.ECS.World.Archetypes (Node (..))
 import Data.Set (Set)
+import GHC.Stack
 
-class ArrowDynamicSystem q arr | arr -> q where
-  -- | Map all matching entities, storing the updated entities.
-  mapDyn :: Set ComponentID -> q i o -> arr i [o]
+-- | Monadic dynamic system.
+--
+-- @since 9.0
+class (Monad m) => MonadDynamicSystem q m | m -> q where
+  -- | Map all matching entities with a query.
+  --
+  -- @since 9.0
+  mapDyn :: i -> Set ComponentID -> q i o -> m [o]
 
-  mapSingleDyn :: Set ComponentID -> q i o -> arr i o
+  -- | Map a single matching entity with a query, or @Nothing@.
+  --
+  -- @since 9.0
+  mapSingleMaybeDyn :: i -> Set ComponentID -> q i a -> m (Maybe a)
 
-  mapSingleMaybeDyn :: Set ComponentID -> q i o -> arr i (Maybe o)
+  -- | Map a single matching entity with a query.
+  mapSingleDyn :: (HasCallStack) => i -> Set ComponentID -> q i o -> m o
+  mapSingleDyn i cIds q = do
+    res <- mapSingleMaybeDyn i cIds q
+    case res of
+      Just a -> return a
+      Nothing -> error "Expected a single matching entity."
 
-  filterMapDyn ::
-    Set ComponentID ->
-    q i o ->
-    (Node -> Bool) ->
-    arr i [o]
+  -- | Map all matching entities with a query and filter.
+  --
+  -- @since 9.0
+  filterMapDyn :: i -> Set ComponentID -> (Node -> Bool) -> q i a -> m [a]
diff --git a/src/Aztecs/ECS/System/Dynamic/Reader.hs b/src/Aztecs/ECS/System/Dynamic/Reader.hs
deleted file mode 100644
--- a/src/Aztecs/ECS/System/Dynamic/Reader.hs
+++ /dev/null
@@ -1,79 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DeriveFunctor #-}
-{-# LANGUAGE FlexibleInstances #-}
-{-# LANGUAGE MultiParamTypeClasses #-}
-
-module Aztecs.ECS.System.Dynamic.Reader
-  ( DynamicReaderSystem,
-    DynamicReaderSystemT (..),
-    ArrowDynamicReaderSystem (..),
-    ArrowQueueSystem (..),
-    raceDyn,
-  )
-where
-
-import Aztecs.ECS.Access
-import Aztecs.ECS.Query.Dynamic.Reader (DynamicQueryReader (..), runDynQueryReader)
-import Aztecs.ECS.System.Dynamic.Reader.Class
-import Aztecs.ECS.System.Queue (ArrowQueueSystem (..))
-import qualified Aztecs.ECS.View as V
-import qualified Aztecs.ECS.World.Archetype as A
-import Aztecs.ECS.World.Bundle
-import Aztecs.ECS.World.Entities (Entities (..))
-import Control.Arrow
-import Control.Category
-import Control.Monad.Identity
-import Control.Parallel (par)
-import qualified Data.Map as Map
-import Prelude hiding (id, (.))
-
-type DynamicReaderSystem = DynamicReaderSystemT Identity
-
-newtype DynamicReaderSystemT m i o = DynamicReaderSystem
-  { -- | Run a dynamic system producing some output
-    runReaderSystemDyn :: Entities -> i -> (o, AccessT m (), DynamicReaderSystemT m i o)
-  }
-  deriving (Functor)
-
-instance (Monad m) => Category (DynamicReaderSystemT m) where
-  id = DynamicReaderSystem $ \_ i -> (i, pure (), id)
-  DynamicReaderSystem f . DynamicReaderSystem g = DynamicReaderSystem $ \w i ->
-    let (b, gAccess, g') = g w i
-        (c, fAccess, f') = f w b
-     in (c, gAccess >> fAccess, f' . g')
-
-instance (Monad m) => Arrow (DynamicReaderSystemT m) where
-  arr f = DynamicReaderSystem $ \_ i -> (f i, pure (), arr f)
-  first (DynamicReaderSystem f) = DynamicReaderSystem $ \w (i, x) ->
-    let (a, access, f') = f w i in ((a, x), access, first f')
-
-instance (Monad m) => ArrowChoice (DynamicReaderSystemT m) where
-  left (DynamicReaderSystem f) = DynamicReaderSystem $ \w i -> case i of
-    Left b -> let (c, access, f') = f w b in (Left c, access, left f')
-    Right d -> (Right d, pure (), left (DynamicReaderSystem f))
-
-instance (Monad m) => ArrowLoop (DynamicReaderSystemT m) where
-  loop (DynamicReaderSystem f) = DynamicReaderSystem $ \w b ->
-    let ((c, d), access, f') = f w (b, d) in (c, access, loop f')
-
-instance (Monad m) => ArrowDynamicReaderSystem DynamicQueryReader (DynamicReaderSystemT m) where
-  allDyn cIds q = DynamicReaderSystem $ \w i ->
-    let !v = V.view cIds $ archetypes w
-     in if V.null v
-          then (runDynQueryReader i q (Map.keys $ entities w) A.empty, pure (), allDyn cIds q)
-          else (V.readAllDyn i q v, pure (), allDyn cIds q)
-  filterDyn cIds q f = DynamicReaderSystem $ \w i ->
-    let !v = V.filterView cIds f $ archetypes w
-     in (V.readAllDyn i q v, pure (), filterDyn cIds q f)
-
-instance (Monad m) => ArrowQueueSystem Bundle (AccessT m) (DynamicReaderSystemT m) where
-  queue f = DynamicReaderSystem $ \_ i -> let !a = f i in ((), a, queue f)
-
-raceDyn :: (Monad m) => DynamicReaderSystemT m i a -> DynamicReaderSystemT m i b -> DynamicReaderSystemT m i (a, b)
-raceDyn (DynamicReaderSystem f) (DynamicReaderSystem g) = DynamicReaderSystem $ \w i ->
-  let fa = f w i
-      gb = g w i
-      gbPar = fa `par` gb
-      (a, fAccess, f') = fa
-      (b, gAccess, g') = gbPar
-   in ((a, b), fAccess >> gAccess, raceDyn f' g')
diff --git a/src/Aztecs/ECS/System/Dynamic/Reader/Class.hs b/src/Aztecs/ECS/System/Dynamic/Reader/Class.hs
--- a/src/Aztecs/ECS/System/Dynamic/Reader/Class.hs
+++ b/src/Aztecs/ECS/System/Dynamic/Reader/Class.hs
@@ -1,22 +1,50 @@
 {-# LANGUAGE FunctionalDependencies #-}
 
-module Aztecs.ECS.System.Dynamic.Reader.Class (ArrowDynamicReaderSystem (..)) where
+-- |
+-- Module      : Aztecs.ECS.System.Dynamic.Reader.Class
+-- Copyright   : (c) Matt Hunzinger, 2025
+-- License     : BSD-style (see the LICENSE file in the distribution)
+--
+-- Maintainer  : matt@hunzinger.me
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
+module Aztecs.ECS.System.Dynamic.Reader.Class (MonadDynamicReaderSystem (..)) where
 
-import Aztecs.ECS.Component (ComponentID)
+import Aztecs.ECS.Component
 import Aztecs.ECS.World.Archetypes (Node)
-import Control.Arrow (Arrow (..), (>>>))
 import Data.Set (Set)
+import GHC.Stack
 
-class (Arrow arr) => ArrowDynamicReaderSystem q arr | arr -> q where
-  allDyn :: Set ComponentID -> q i o -> arr i [o]
+-- | Monadic dynamic reader system.
+--
+-- @since 9.0
+class (Monad m) => MonadDynamicReaderSystem q m | m -> q where
+  -- | Match all entities with a query.
+  --
+  -- @since 9.0
+  allDyn :: i -> Set ComponentID -> q i o -> m [o]
 
-  filterDyn :: Set ComponentID -> q i o -> (Node -> Bool) -> arr i [o]
+  -- | Match a single entity with a query.
+  --
+  -- @since 9.0
+  singleDyn :: (HasCallStack) => i -> Set ComponentID -> q i o -> m o
+  singleDyn i cIds q = do
+    os <- allDyn i cIds q
+    case os of
+      [o] -> return o
+      _ -> error "singleDyn: expected a single result, but got multiple"
 
-  singleDyn :: Set ComponentID -> q () a -> arr () a
-  singleDyn cIds q =
-    allDyn cIds q
-      >>> arr
-        ( \as -> case as of
-            [a] -> a
-            _ -> error "TODO"
-        )
+  -- | Match a single entity with a query, or Nothing.
+  --
+  -- @since 9.0
+  singleMaybeDyn :: i -> Set ComponentID -> q i o -> m (Maybe o)
+  singleMaybeDyn i cIds q = do
+    os <- allDyn i cIds q
+    return $ case os of
+      [o] -> Just o
+      _ -> Nothing
+
+  -- | Match all entities with a query and filter.
+  --
+  -- @since 9.0
+  filterDyn :: i -> Set ComponentID -> q i a -> (Node -> Bool) -> m [a]
diff --git a/src/Aztecs/ECS/System/Queue.hs b/src/Aztecs/ECS/System/Queue.hs
deleted file mode 100644
--- a/src/Aztecs/ECS/System/Queue.hs
+++ /dev/null
@@ -1,31 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DeriveFunctor #-}
-{-# LANGUAGE FlexibleInstances #-}
-{-# LANGUAGE MultiParamTypeClasses #-}
-
-module Aztecs.ECS.System.Queue (QueueSystem (..), ArrowQueueSystem (..)) where
-
-import Aztecs.ECS.Access (Access)
-import Aztecs.ECS.System.Queue.Class (ArrowQueueSystem (..))
-import Aztecs.ECS.World.Bundle (Bundle)
-import Control.Arrow (Arrow (..))
-import Control.Category (Category (..))
-
--- | System that can queue `Access` to a `World`.
-newtype QueueSystem i o = QueueSystem {runQueueSystem :: i -> (o, Access ())}
-  deriving (Functor)
-
-instance Category QueueSystem where
-  id = QueueSystem $ \i -> (i, pure ())
-  QueueSystem f . QueueSystem g = QueueSystem $ \i ->
-    let (b, access) = g i
-        (c, access') = f b
-     in (c, access >> access')
-
-instance Arrow QueueSystem where
-  arr f = QueueSystem $ \i -> (f i, pure ())
-  first (QueueSystem f) = QueueSystem $ \(b, d) ->
-    let (c, access) = f b in ((c, d), access)
-
-instance ArrowQueueSystem Bundle Access QueueSystem where
-  queue f = QueueSystem $ \i -> let !a = f i in ((), a)
diff --git a/src/Aztecs/ECS/System/Queue/Class.hs b/src/Aztecs/ECS/System/Queue/Class.hs
deleted file mode 100644
--- a/src/Aztecs/ECS/System/Queue/Class.hs
+++ /dev/null
@@ -1,10 +0,0 @@
-{-# LANGUAGE FunctionalDependencies #-}
-
-module Aztecs.ECS.System.Queue.Class (ArrowQueueSystem (..)) where
-
-import Aztecs.ECS.Access (MonadAccess)
-import Control.Arrow (Arrow (..))
-
-class (MonadAccess b m, Arrow arr) => ArrowQueueSystem b m arr | arr -> m where
-  -- | Queue an `Access` to happen after this system schedule.
-  queue :: (i -> m ()) -> arr i ()
diff --git a/src/Aztecs/ECS/System/Reader.hs b/src/Aztecs/ECS/System/Reader.hs
deleted file mode 100644
--- a/src/Aztecs/ECS/System/Reader.hs
+++ /dev/null
@@ -1,72 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DeriveFunctor #-}
-{-# LANGUAGE FlexibleInstances #-}
-{-# LANGUAGE MultiParamTypeClasses #-}
-
-module Aztecs.ECS.System.Reader
-  ( ReaderSystem,
-    ReaderSystemT (..),
-    ArrowReaderSystem (..),
-    ArrowQueueSystem (..),
-  )
-where
-
-import Aztecs.ECS.Access (AccessT)
-import Aztecs.ECS.Query.Reader
-import Aztecs.ECS.System.Dynamic.Reader
-import Aztecs.ECS.System.Reader.Class (ArrowReaderSystem (..))
-import qualified Aztecs.ECS.World.Archetype as A
-import Aztecs.ECS.World.Archetypes (Node (..))
-import Aztecs.ECS.World.Bundle (Bundle)
-import Aztecs.ECS.World.Components (ComponentID, Components)
-import Control.Arrow
-import Control.Category
-import Control.Monad.Identity
-import qualified Data.Foldable as F
-import Data.Set (Set)
-import Prelude hiding (id, (.))
-
-type ReaderSystem = ReaderSystemT Identity
-
--- | System to process entities.
-newtype ReaderSystemT m i o = ReaderSystem
-  { -- | Run a system, producing a `DynamicSystem` that can be repeatedly run.
-    runReaderSystem :: Components -> (DynamicReaderSystemT m i o, Set ComponentID, Components)
-  }
-  deriving (Functor)
-
-instance (Monad m) => Category (ReaderSystemT m) where
-  id = ReaderSystem $ \cs -> (id, mempty, cs)
-  ReaderSystem f . ReaderSystem g = ReaderSystem $ \cs ->
-    let (f', rwsF, cs') = f cs
-        (g', rwsG, cs'') = g cs'
-     in (f' . g', rwsF <> rwsG, cs'')
-
-instance (Monad m) => Arrow (ReaderSystemT m) where
-  arr f = ReaderSystem $ \cs -> (arr f, mempty, cs)
-  first (ReaderSystem f) = ReaderSystem $ \cs ->
-    let (f', rwsF, cs') = f cs in (first f', rwsF, cs')
-  f &&& g = ReaderSystem $ \cs ->
-    let (dynF, rwsA, cs') = runReaderSystem f cs
-        (dynG, rwsB, cs'') = runReaderSystem g cs'
-     in (raceDyn dynF dynG, rwsA <> rwsB, cs'')
-
-instance (Monad m) => ArrowChoice (ReaderSystemT m) where
-  left (ReaderSystem f) = ReaderSystem $ \cs -> let (f', rwsF, cs') = f cs in (left f', rwsF, cs')
-
-instance (Monad m) => ArrowLoop (ReaderSystemT m) where
-  loop (ReaderSystem f) = ReaderSystem $ \cs -> let (f', rwsF, cs') = f cs in (loop f', rwsF, cs')
-
-instance (Monad m) => ArrowReaderSystem QueryReader (ReaderSystemT m) where
-  all q = ReaderSystem $ \cs ->
-    let !(rs, cs', dynQ) = runQueryReader q cs in (allDyn rs dynQ, rs, cs')
-  filter q qf = ReaderSystem $ \cs ->
-    let !(rs, cs', dynQ) = runQueryReader q cs
-        !(dynQf, cs'') = runQueryFilter qf cs'
-        qf' n =
-          F.all (\cId -> A.member cId $ nodeArchetype n) (filterWith dynQf)
-            && F.all (\cId -> not (A.member cId $ nodeArchetype n)) (filterWithout dynQf)
-     in (filterDyn rs dynQ qf', rs, cs'')
-
-instance (Monad m) => ArrowQueueSystem Bundle (AccessT m) (ReaderSystemT m) where
-  queue f = ReaderSystem $ \cs -> (queue f, mempty, cs)
diff --git a/src/Aztecs/ECS/System/Reader/Class.hs b/src/Aztecs/ECS/System/Reader/Class.hs
--- a/src/Aztecs/ECS/System/Reader/Class.hs
+++ b/src/Aztecs/ECS/System/Reader/Class.hs
@@ -1,26 +1,49 @@
-{-# LANGUAGE AllowAmbiguousTypes #-}
 {-# LANGUAGE FunctionalDependencies #-}
 
-module Aztecs.ECS.System.Reader.Class (ArrowReaderSystem (..)) where
+-- |
+-- Module      : Aztecs.ECS.System.Reader.Class
+-- Copyright   : (c) Matt Hunzinger, 2025
+-- License     : BSD-style (see the LICENSE file in the distribution)
+--
+-- Maintainer  : matt@hunzinger.me
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
+module Aztecs.ECS.System.Reader.Class (MonadReaderSystem (..)) where
 
 import Aztecs.ECS.Query.Reader (QueryFilter (..))
-import Control.Arrow (Arrow (..), (>>>))
-import Prelude hiding (all, any, filter, id, lookup, map, mapM, reads, (.))
+import GHC.Stack
+import Prelude hiding (all)
 
-class (Arrow arr) => ArrowReaderSystem q arr | arr -> q where
-  -- | Query all matching entities.
-  all :: q i a -> arr i [a]
+-- | Monadic reader system.
+--
+-- @since 9.0
+class (Monad m) => MonadReaderSystem q m | m -> q where
+  -- | Match all entities with a query.
+  --
+  -- @since 9.0
+  all :: i -> q i o -> m [o]
 
-  -- | Query all matching entities with a `QueryFilter`.
-  filter :: q () a -> QueryFilter -> arr () [a]
+  -- | Match a single entity with a query.
+  --
+  -- @since 9.0
+  single :: (HasCallStack) => i -> q i o -> m o
+  single i q = do
+    os <- all i q
+    case os of
+      [o] -> return o
+      _ -> error "single: expected a single result"
 
-  -- | Query a single matching entity.
-  -- If there are zero or multiple matching entities, an error will be thrown.
-  single :: q i a -> arr i a
-  single q =
-    all q
-      >>> arr
-        ( \as -> case as of
-            [a] -> a
-            _ -> error "TODO"
-        )
+  -- | Match a single entity with a query, or @Nothing@.
+  --
+  -- @since 9.0
+  singleMaybe :: i -> q i o -> m (Maybe o)
+  singleMaybe i q = do
+    os <- all i q
+    return $ case os of
+      [o] -> Just o
+      _ -> Nothing
+
+  -- | Match all entities with a query and filter.
+  --
+  -- @since 9.0
+  filter :: i -> q i o -> QueryFilter -> m [o]
diff --git a/src/Aztecs/ECS/View.hs b/src/Aztecs/ECS/View.hs
--- a/src/Aztecs/ECS/View.hs
+++ b/src/Aztecs/ECS/View.hs
@@ -3,6 +3,14 @@
 {-# LANGUAGE RankNTypes #-}
 {-# LANGUAGE ScopedTypeVariables #-}
 
+-- |
+-- Module      : Aztecs.ECS.View
+-- Copyright   : (c) Matt Hunzinger, 2025
+-- License     : BSD-style (see the LICENSE file in the distribution)
+--
+-- Maintainer  : matt@hunzinger.me
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
 module Aztecs.ECS.View
   ( View (..),
     view,
@@ -12,39 +20,52 @@
     unview,
     allDyn,
     singleDyn,
-    readAllDyn,
+    mapDyn,
+    mapSingleDyn,
   )
 where
 
-import Aztecs.ECS.Query.Dynamic (DynamicQuery (..))
-import Aztecs.ECS.Query.Dynamic.Reader (DynamicQueryReader (..), runDynQueryReader)
-import qualified Aztecs.ECS.World.Archetype as A
-import Aztecs.ECS.World.Archetypes (ArchetypeID, Archetypes, Node (..))
+import Aztecs.ECS.Query.Dynamic (DynamicQueryT (..))
+import Aztecs.ECS.Query.Dynamic.Reader (DynamicQueryReaderT (..), runDynQueryReaderT)
+import Aztecs.ECS.World.Archetypes
 import qualified Aztecs.ECS.World.Archetypes as AS
-import Aztecs.ECS.World.Components (ComponentID)
+import Aztecs.ECS.World.Components
 import Aztecs.ECS.World.Entities (Entities)
 import qualified Aztecs.ECS.World.Entities as E
-import Data.Foldable (foldl')
+import Data.Foldable (foldl', foldlM)
 import Data.Map.Strict (Map)
 import qualified Data.Map.Strict as Map
 import Data.Set (Set)
-import qualified Data.Set as Set
 import Prelude hiding (null)
 
 -- | View into a `World`, containing a subset of archetypes.
-newtype View = View {viewArchetypes :: Map ArchetypeID Node}
+--
+-- @since 9.0
+newtype View = View
+  { -- | Archetypes contained in this view.
+    --
+    -- @since 9.0
+    viewArchetypes :: Map ArchetypeID Node
+  }
   deriving (Show, Semigroup, Monoid)
 
 -- | View into all archetypes containing the provided component IDs.
+--
+-- @since 9.0
 view :: Set ComponentID -> Archetypes -> View
 view cIds as = View $ AS.find cIds as
 
+-- | View into a single archetype containing the provided component IDs.
+--
+-- @since 9.0
 viewSingle :: Set ComponentID -> Archetypes -> Maybe View
 viewSingle cIds as = case Map.toList $ AS.find cIds as of
   [a] -> Just . View $ uncurry Map.singleton a
   _ -> Nothing
 
 -- | View into all archetypes containing the provided component IDs and matching the provided predicate.
+--
+-- @since 9.0
 filterView ::
   Set ComponentID ->
   (Node -> Bool) ->
@@ -52,10 +73,15 @@
   View
 filterView cIds f as = View $ Map.filter f (AS.find cIds as)
 
+-- | @True@ if the `View` is empty.
+--
+-- @since 9.0
 null :: View -> Bool
 null = Map.null . viewArchetypes
 
 -- | "Un-view" a `View` back into a `World`.
+--
+-- @since 9.0
 unview :: View -> Entities -> Entities
 unview v es =
   es
@@ -67,31 +93,49 @@
     }
 
 -- | Query all matching entities in a `View`.
-allDyn :: i -> DynamicQuery i a -> View -> ([a], View)
+--
+-- @since 9.0
+allDyn :: (Monad m) => i -> DynamicQueryReaderT m i a -> View -> m [a]
 allDyn i q v =
-  let (as, arches) =
-        foldl'
-          ( \(acc, archAcc) (aId, n) ->
-              let (as', arch') = runDynQuery q (repeat i) (Set.toList . A.entities $ nodeArchetype n) (nodeArchetype n)
-               in (as' ++ acc, Map.insert aId (n {nodeArchetype = arch'}) archAcc)
-          )
-          ([], Map.empty)
-          (Map.toList $ viewArchetypes v)
-   in (as, View arches)
-
--- | Query all matching entities in a `View`.
-singleDyn :: i -> DynamicQuery i a -> View -> (Maybe a, View)
-singleDyn i q v = case allDyn i q v of
-  -- TODO [a], removing this errors for now
-  (a : _, v') -> (Just a, v')
-  _ -> (Nothing, v)
-
--- | Query all matching entities in a `View`.
-readAllDyn :: i -> DynamicQueryReader i a -> View -> [a]
-readAllDyn i q v =
-  foldl'
-    ( \acc n ->
-        runDynQueryReader i q (Set.toList . A.entities $ nodeArchetype n) (nodeArchetype n) ++ acc
+  foldlM
+    ( \acc n -> do
+        as <- runDynQueryReaderT i q $ nodeArchetype n
+        return $ as ++ acc
     )
     []
     (viewArchetypes v)
+
+-- | Query all matching entities in a `View`.
+--
+-- @since 9.0
+singleDyn :: (Monad m) => i -> DynamicQueryReaderT m i a -> View -> m (Maybe a)
+singleDyn i q v = do
+  as <- allDyn i q v
+  return $ case as of
+    [a] -> Just a
+    _ -> Nothing
+
+-- | Map all matching entities in a `View`.
+--
+-- @since 9.0
+mapDyn :: (Monad m) => i -> DynamicQueryT m i a -> View -> m ([a], View)
+mapDyn i q v = do
+  (as, arches) <-
+    foldlM
+      ( \(acc, archAcc) (aId, n) -> do
+          (as', arch') <- runDynQuery q (repeat i) $ nodeArchetype n
+          return (as' ++ acc, Map.insert aId (n {nodeArchetype = arch'}) archAcc)
+      )
+      ([], Map.empty)
+      (Map.toList $ viewArchetypes v)
+  return (as, View arches)
+
+-- | Map a single matching entity in a `View`.
+--
+-- @since 9.0
+mapSingleDyn :: (Monad m) => i -> DynamicQueryT m i a -> View -> m (Maybe a, View)
+mapSingleDyn i q v = do
+  (as, arches) <- mapDyn i q v
+  return $ case as of
+    [a] -> (Just a, arches)
+    _ -> (Nothing, arches)
diff --git a/src/Aztecs/ECS/World.hs b/src/Aztecs/ECS/World.hs
--- a/src/Aztecs/ECS/World.hs
+++ b/src/Aztecs/ECS/World.hs
@@ -1,19 +1,24 @@
 {-# LANGUAGE AllowAmbiguousTypes #-}
-{-# LANGUAGE BangPatterns #-}
 {-# LANGUAGE DeriveAnyClass #-}
 {-# LANGUAGE DeriveGeneric #-}
 {-# LANGUAGE FlexibleContexts #-}
 {-# LANGUAGE ScopedTypeVariables #-}
 {-# LANGUAGE TypeFamilies #-}
 
+-- |
+-- Module      : Aztecs.ECS.World
+-- Copyright   : (c) Matt Hunzinger, 2025
+-- License     : BSD-style (see the LICENSE file in the distribution)
+--
+-- Maintainer  : matt@hunzinger.me
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
 module Aztecs.ECS.World
   ( World (..),
     empty,
     spawn,
-    spawnWithArchetypeId,
     spawnEmpty,
     insert,
-    insertWithId,
     lookup,
     remove,
     removeWithId,
@@ -23,24 +28,33 @@
 
 import Aztecs.ECS.Component
 import Aztecs.ECS.Entity
-import Aztecs.ECS.World.Archetypes (ArchetypeID)
 import Aztecs.ECS.World.Bundle
 import Aztecs.ECS.World.Entities (Entities)
 import qualified Aztecs.ECS.World.Entities as E
 import Control.DeepSeq
 import Data.Dynamic
-import Data.Map (Map)
+import Data.IntMap (IntMap)
 import GHC.Generics
 import Prelude hiding (lookup)
 
 -- | World of entities and their components.
+--
+-- @since 9.0
 data World = World
-  { entities :: Entities,
+  { -- | Entities and their components.
+    --
+    -- @since 9.0
+    entities :: !Entities,
+    -- | Next unique entity identifier.
+    --
+    -- @since 9.0
     nextEntityId :: !EntityID
   }
   deriving (Show, Generic, NFData)
 
 -- | Empty `World`.
+--
+-- @since 9.0
 empty :: World
 empty =
   World
@@ -48,46 +62,44 @@
       nextEntityId = EntityID 0
     }
 
+-- | Spawn a `Bundle` into the `World`.
+--
+-- @since 9.0
 spawn :: Bundle -> World -> (EntityID, World)
 spawn b w =
   let e = nextEntityId w
    in (e, w {entities = E.spawn e b $ entities w, nextEntityId = EntityID $ unEntityId e + 1})
 
 -- | Spawn an empty entity.
+--
+-- @since 9.0
 spawnEmpty :: World -> (EntityID, World)
 spawnEmpty w = let e = nextEntityId w in (e, w {nextEntityId = EntityID $ unEntityId e + 1})
 
--- | Spawn an entity with a component and its `ComponentID` directly into an archetype.
-spawnWithArchetypeId ::
-  forall a.
-  (Component a) =>
-  ArchetypeID ->
-  ComponentID ->
-  a ->
-  World ->
-  (EntityID, World)
-spawnWithArchetypeId aId cId c w =
-  let !(e, w') = spawnEmpty w
-   in (e, w' {entities = E.spawnWithArchetypeId e aId cId c (entities w')})
-
--- | Insert a component into an entity.
-insert :: forall a. (Component a) => EntityID -> a -> World -> World
+-- | Insert a `Bundle` into an entity.
+--
+-- @since 9.0
+insert :: EntityID -> Bundle -> World -> World
 insert e c w = w {entities = E.insert e c (entities w)}
 
--- | Insert a component into an entity with its `ComponentID`.
-insertWithId :: (Component a) => EntityID -> ComponentID -> a -> World -> World
-insertWithId e cId c w = w {entities = E.insertWithId e cId c (entities w)}
-
+-- | Lookup a component in an entity.
+--
+-- @since 9.0
 lookup :: forall a. (Component a) => EntityID -> World -> Maybe a
 lookup e w = E.lookup e $ entities w
 
--- | Insert a component into an entity.
+-- | Remove a component from an entity.
+--
+-- @since 9.0
 remove :: forall a. (Component a) => EntityID -> World -> (Maybe a, World)
 remove e w = let (a, es) = E.remove e (entities w) in (a, w {entities = es})
 
+-- | Remove a component from an entity with its `ComponentID`.
+--
+-- @since 9.0
 removeWithId :: forall a. (Component a) => EntityID -> ComponentID -> World -> (Maybe a, World)
 removeWithId e cId w = let (a, es) = E.removeWithId e cId (entities w) in (a, w {entities = es})
 
 -- | Despawn an entity, returning its components.
-despawn :: EntityID -> World -> (Map ComponentID Dynamic, World)
+despawn :: EntityID -> World -> (IntMap Dynamic, World)
 despawn e w = let (a, es) = E.despawn e (entities w) in (a, w {entities = es})
diff --git a/src/Aztecs/ECS/World/Archetype.hs b/src/Aztecs/ECS/World/Archetype.hs
--- a/src/Aztecs/ECS/World/Archetype.hs
+++ b/src/Aztecs/ECS/World/Archetype.hs
@@ -11,16 +11,32 @@
 {-# LANGUAGE TypeApplications #-}
 {-# LANGUAGE TypeFamilies #-}
 
+-- |
+-- Module      : Aztecs.ECS.World.Archetype
+-- Copyright   : (c) Matt Hunzinger, 2025
+-- License     : BSD-style (see the LICENSE file in the distribution)
+--
+-- Maintainer  : matt@hunzinger.me
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
 module Aztecs.ECS.World.Archetype
   ( Archetype (..),
     empty,
+    singleton,
     lookupComponent,
+    lookupComponents,
+    lookupComponentsAsc,
+    lookupComponentsAscMaybe,
     lookupStorage,
     member,
     remove,
     removeStorages,
     insertComponent,
+    insertComponents,
     insertAscList,
+    zipWith,
+    zipWith_,
+    zipWithM,
   )
 where
 
@@ -30,77 +46,205 @@
 import Aztecs.ECS.World.Storage.Dynamic
 import qualified Aztecs.ECS.World.Storage.Dynamic as S
 import Control.DeepSeq
+import Control.Monad.Writer hiding (zipWithM)
 import Data.Dynamic
 import Data.Foldable
-import Data.Map.Strict (Map)
-import qualified Data.Map.Strict as Map
+import Data.IntMap (IntMap)
+import qualified Data.IntMap.Strict as IntMap
+import Data.Map (Map)
+import qualified Data.Map as Map
+import Data.Maybe
 import Data.Set (Set)
 import qualified Data.Set as Set
 import GHC.Generics
+import Prelude hiding (map, zipWith)
 
+-- | Archetype of entities and components.
+-- An archetype is guranteed to contain one of each stored component per entity.
+--
+-- @since 9.0
 data Archetype = Archetype
-  { storages :: !(Map ComponentID DynamicStorage),
+  { -- | Component storages.
+    --
+    -- @since 9.0
+    storages :: !(IntMap DynamicStorage),
+    -- | Entities stored in this archetype.
+    --
+    -- @since 9.0
     entities :: !(Set EntityID)
   }
   deriving (Show, Generic, NFData)
 
+-- | Empty archetype.
+--
+-- @since 9.0
 empty :: Archetype
-empty = Archetype {storages = Map.empty, entities = Set.empty}
+empty = Archetype {storages = IntMap.empty, entities = Set.empty}
 
+-- | Archetype with a single entity.
+--
+-- @since 9.0
+singleton :: EntityID -> Archetype
+singleton e = Archetype {storages = IntMap.empty, entities = Set.singleton e}
+
+-- | Lookup a component `Storage` by its `ComponentID`.
+--
+-- @since 9.0
+{-# INLINE lookupStorage #-}
 lookupStorage :: (Component a) => ComponentID -> Archetype -> Maybe (StorageT a)
 lookupStorage cId w = do
-  dynS <- Map.lookup cId (storages w)
-  fromDynamic (storageDyn dynS)
+  !dynS <- IntMap.lookup (unComponentId cId) $ storages w
+  fromDynamic $ storageDyn dynS
 
-lookupComponents :: forall a. (Component a) => ComponentID -> Archetype -> Map EntityID a
-lookupComponents cId arch = case lookupStorage @a cId arch of
-  Just s -> Map.fromAscList . zip (Set.toList $ entities arch) $ S.toAscList s
+-- | Lookup a component by its `EntityID` and `ComponentID`.
+--
+-- @since 9.0
+{-# INLINE lookupComponent #-}
+lookupComponent :: (Component a) => EntityID -> ComponentID -> Archetype -> Maybe a
+lookupComponent e cId w = lookupComponents cId w Map.!? e
+
+-- | Lookup all components by their `ComponentID`.
+--
+-- @since 9.0
+{-# INLINE lookupComponents #-}
+lookupComponents :: (Component a) => ComponentID -> Archetype -> Map EntityID a
+lookupComponents cId arch = case lookupComponentsAscMaybe cId arch of
+  Just as -> Map.fromAscList $ zip (Set.toList $ entities arch) as
   Nothing -> Map.empty
 
-insertComponent :: forall a. (Component a) => EntityID -> ComponentID -> a -> Archetype -> Archetype
+-- | Lookup all components by their `ComponentID`, in ascending order by their `EntityID`.
+--
+-- @since 9.0
+{-# INLINE lookupComponentsAsc #-}
+lookupComponentsAsc :: (Component a) => ComponentID -> Archetype -> [a]
+lookupComponentsAsc cId = fromMaybe [] . lookupComponentsAscMaybe cId
+
+-- | Lookup all components by their `ComponentID`, in ascending order by their `EntityID`.
+--
+-- @since 9.0
+{-# INLINE lookupComponentsAscMaybe #-}
+lookupComponentsAscMaybe :: forall a. (Component a) => ComponentID -> Archetype -> Maybe [a]
+lookupComponentsAscMaybe cId arch = S.toAscList <$> lookupStorage @a cId arch
+
+-- | Insert a component into the archetype.
+-- This assumes the archetype contains one of each stored component per entity.
+--
+-- @since 9.0
+insertComponent ::
+  forall a. (Component a) => EntityID -> ComponentID -> a -> Archetype -> Archetype
 insertComponent e cId c arch =
   let !storage =
         S.fromAscList @a @(StorageT a) . Map.elems . Map.insert e c $ lookupComponents cId arch
-   in arch {storages = Map.insert cId (dynStorage @a storage) (storages arch)}
+   in arch {storages = IntMap.insert (unComponentId cId) (dynStorage @a storage) (storages arch)}
 
+-- | @True@ if this archetype contains an entity with the provided `ComponentID`.
+--
+-- @since 9.0
 member :: ComponentID -> Archetype -> Bool
-member cId arch = Map.member cId (storages arch)
+member cId = IntMap.member (unComponentId cId) . storages
 
-lookupComponent :: forall a. (Component a) => EntityID -> ComponentID -> Archetype -> Maybe a
-lookupComponent e cId w = lookupComponents cId w Map.!? e
+-- | Zip a list of components with a function and a component storage.
+--
+-- @since 9.0
+{-# INLINE zipWith #-}
+zipWith ::
+  forall a c. (Component c) => [a] -> (a -> c -> c) -> ComponentID -> Archetype -> ([c], Archetype)
+zipWith as f cId arch =
+  let go maybeDyn = case maybeDyn of
+        Just dyn -> case fromDynamic $ storageDyn dyn of
+          Just s -> do
+            let !(cs', s') = S.zipWith @c @(StorageT c) f as s
+            tell cs'
+            return $ Just $ dyn {storageDyn = toDyn s'}
+          Nothing -> return maybeDyn
+        Nothing -> return Nothing
+      !(storages', cs) = runWriter $ IntMap.alterF go (unComponentId cId) $ storages arch
+   in (cs, arch {storages = storages'})
 
+-- | Zip a list of components with a monadic function and a component storage.
+--
+-- @since 9.0
+zipWithM ::
+  forall m a c. (Monad m, Component c) => [a] -> (a -> c -> m c) -> ComponentID -> Archetype -> m ([c], Archetype)
+zipWithM as f cId arch = do
+  let go maybeDyn = case maybeDyn of
+        Just dyn -> case fromDynamic $ storageDyn dyn of
+          Just s -> do
+            (cs', s') <- lift $ S.zipWithM @c @(StorageT c) f as s
+            tell cs'
+            return $ Just $ dyn {storageDyn = toDyn s'}
+          Nothing -> return maybeDyn
+        Nothing -> return Nothing
+  (storages', cs) <- runWriterT $ IntMap.alterF go (unComponentId cId) $ storages arch
+  return (cs, arch {storages = storages'})
+
+-- | Zip a list of components with a function and a component storage.
+--
+-- @since 9.0
+{-# INLINE zipWith_ #-}
+zipWith_ ::
+  forall a c. (Component c) => [a] -> (a -> c -> c) -> ComponentID -> Archetype -> Archetype
+zipWith_ as f cId arch =
+  let go maybeDyn = case maybeDyn of
+        Just dyn -> case fromDynamic $ storageDyn dyn of
+          Just s ->
+            let !s' = S.zipWith_ @c @(StorageT c) f as s in Just $ dyn {storageDyn = toDyn s'}
+          Nothing -> maybeDyn
+        Nothing -> Nothing
+      !storages' = IntMap.alter go (unComponentId cId) $ storages arch
+   in (arch {storages = storages'})
+
 -- | Insert a list of components into the archetype, sorted in ascending order by their `EntityID`.
+--
+-- @since 9.0
+{-# INLINE insertAscList #-}
 insertAscList :: forall a. (Component a) => ComponentID -> [a] -> Archetype -> Archetype
 insertAscList cId as arch =
   let !storage = dynStorage @a $ S.fromAscList @a @(StorageT a) as
-   in arch {storages = Map.insert cId storage (storages arch)}
+   in arch {storages = IntMap.insert (unComponentId cId) storage $ storages arch}
 
-remove :: EntityID -> Archetype -> (Map ComponentID Dynamic, Archetype)
+-- | Remove an entity from an archetype, returning its components.
+--
+-- @since 9.0
+remove :: EntityID -> Archetype -> (IntMap Dynamic, Archetype)
 remove e arch =
-  foldl'
-    ( \(dynAcc, archAcc) (cId, dynS) ->
+  let go (dynAcc, archAcc) (cId, dynS) =
         let cs = Map.fromAscList . zip (Set.toList $ entities arch) $ toAscListDyn dynS
             !(dynA, cs') = Map.updateLookupWithKey (\_ _ -> Nothing) e cs
             dynS' = S.fromAscListDyn (Map.elems cs') dynS
             !dynAcc' = case dynA of
-              Just d -> Map.insert cId d dynAcc
+              Just d -> IntMap.insert cId d dynAcc
               Nothing -> dynAcc
-         in (dynAcc', archAcc {storages = Map.insert cId dynS' $ storages archAcc})
-    )
-    (Map.empty, arch)
-    (Map.toList $ storages arch)
+         in (dynAcc', archAcc {storages = IntMap.insert cId dynS' $ storages archAcc})
+      arch' = arch {entities = Set.delete e $ entities arch}
+   in foldl' go (IntMap.empty, arch') . IntMap.toList $ storages arch'
 
-removeStorages :: EntityID -> Archetype -> (Map ComponentID DynamicStorage, Archetype)
+-- | Remove an entity from an archetype, returning its component storages.
+--
+-- @since 9.0
+removeStorages :: EntityID -> Archetype -> (IntMap DynamicStorage, Archetype)
 removeStorages e arch =
-  foldl'
-    ( \(dynAcc, archAcc) (cId, dynS) ->
+  let go (dynAcc, archAcc) (cId, dynS) =
         let cs = Map.fromAscList . zip (Set.toList $ entities arch) $ toAscListDyn dynS
             !(dynA, cs') = Map.updateLookupWithKey (\_ _ -> Nothing) e cs
             dynS' = S.fromAscListDyn (Map.elems cs') dynS
             !dynAcc' = case dynA of
-              Just d -> Map.insert cId (S.singletonDyn d dynS') dynAcc
+              Just d -> IntMap.insert cId (S.singletonDyn d dynS') dynAcc
               Nothing -> dynAcc
-         in (dynAcc', archAcc {storages = Map.insert cId dynS' $ storages archAcc})
-    )
-    (Map.empty, arch)
-    (Map.toList $ storages arch)
+         in (dynAcc', archAcc {storages = IntMap.insert cId dynS' $ storages archAcc})
+      arch' = arch {entities = Set.delete e $ entities arch}
+   in foldl' go (IntMap.empty, arch') . IntMap.toList $ storages arch'
+
+-- | Insert a map of component storages and their `EntityID` into the archetype.
+--
+-- @since 9.0
+insertComponents :: EntityID -> IntMap Dynamic -> Archetype -> Archetype
+insertComponents e cs arch =
+  let f archAcc (itemCId, dyn) =
+        let storages' = IntMap.adjust go itemCId (storages archAcc)
+            es = Set.toList $ entities archAcc
+            go s =
+              let ecs = Map.elems . Map.insert e dyn . Map.fromAscList . zip es $ toAscListDyn s
+               in fromAscListDyn ecs s
+         in archAcc {storages = storages', entities = Set.insert e $ entities archAcc}
+   in foldl' f arch (IntMap.toList cs)
diff --git a/src/Aztecs/ECS/World/Archetypes.hs b/src/Aztecs/ECS/World/Archetypes.hs
--- a/src/Aztecs/ECS/World/Archetypes.hs
+++ b/src/Aztecs/ECS/World/Archetypes.hs
@@ -11,6 +11,14 @@
 {-# LANGUAGE TupleSections #-}
 {-# LANGUAGE TypeFamilies #-}
 
+-- |
+-- Module      : Aztecs.ECS.World.Archetypes
+-- Copyright   : (c) Matt Hunzinger, 2025
+-- License     : BSD-style (see the LICENSE file in the distribution)
+--
+-- Maintainer  : matt@hunzinger.me
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
 module Aztecs.ECS.World.Archetypes
   ( ArchetypeID (..),
     Node (..),
@@ -28,57 +36,74 @@
   )
 where
 
-import Aztecs.ECS.Component (Component (..), ComponentID)
-import Aztecs.ECS.Entity (EntityID (..))
-import Aztecs.ECS.World.Archetype
-  ( Archetype (..),
-    insertComponent,
-    removeStorages,
-  )
+import Aztecs.ECS.Component
+import Aztecs.ECS.Entity
+import Aztecs.ECS.World.Archetype (Archetype (..))
 import qualified Aztecs.ECS.World.Archetype as A
-import Aztecs.ECS.World.Storage.Dynamic (fromAscListDyn, toAscListDyn)
+import Aztecs.ECS.World.Bundle.Dynamic
+import Aztecs.ECS.World.Storage.Dynamic
 import Control.DeepSeq (NFData (..))
-import Data.Dynamic (fromDynamic)
+import Data.Dynamic
 import Data.Foldable (foldl')
+import qualified Data.IntMap.Strict as IntMap
 import Data.Map.Strict (Map)
 import qualified Data.Map.Strict as Map
-import Data.Maybe (mapMaybe)
+import Data.Maybe
 import Data.Set (Set)
 import qualified Data.Set as Set
-import GHC.Generics (Generic)
+import GHC.Generics
 import Prelude hiding (all, lookup, map)
 
 -- | `Archetype` ID.
-newtype ArchetypeID = ArchetypeID {unArchetypeId :: Int}
+--
+-- @since 9.0
+newtype ArchetypeID = ArchetypeID
+  { -- | Unique integer identifier.
+    --
+    -- @since 9.0
+    unArchetypeId :: Int
+  }
   deriving newtype (Eq, Ord, Show, NFData)
 
 -- | Node in `Archetypes`.
+--
+-- @since 9.0
 data Node = Node
   { -- | Unique set of `ComponentID`s of this `Node`.
+    --
+    -- @since 9.0
     nodeComponentIds :: !(Set ComponentID),
     -- | `Archetype` of this `Node`.
-    nodeArchetype :: !Archetype,
-    -- | Edges to other `Archetype`s by adding a `ComponentID`.
-    nodeAdd :: !(Map ComponentID ArchetypeID),
-    -- | Edges to other `Archetype`s by removing a `ComponentID`.
-    nodeRemove :: !(Map ComponentID ArchetypeID)
+    --
+    -- @since 9.0
+    nodeArchetype :: !Archetype
   }
   deriving (Show, Generic, NFData)
 
--- | `Archetype` graph.
+-- | `Archetype` map.
 data Archetypes = Archetypes
-  { -- | Archetype nodes in the graph.
+  { -- | Archetype nodes in the map.
+    --
+    -- @since 9.0
     nodes :: !(Map ArchetypeID Node),
     -- | Mapping of unique `ComponentID` sets to `ArchetypeID`s.
+    --
+    -- @since 9.0
     archetypeIds :: !(Map (Set ComponentID) ArchetypeID),
     -- | Next unique `ArchetypeID`.
+    --
+    -- @since 9.0
     nextArchetypeId :: !ArchetypeID,
     -- | Mapping of `ComponentID`s to `ArchetypeID`s of `Archetypes` that contain them.
+    --
+    -- @since 9.0
     componentIds :: !(Map ComponentID (Set ArchetypeID))
   }
   deriving (Show, Generic, NFData)
 
 -- | Empty `Archetypes`.
+--
+-- @since 9.0
 empty :: Archetypes
 empty =
   Archetypes
@@ -89,6 +114,8 @@
     }
 
 -- | Insert an archetype by its set of `ComponentID`s.
+--
+-- @since 9.0
 insertArchetype :: Set ComponentID -> Node -> Archetypes -> (ArchetypeID, Archetypes)
 insertArchetype cIds n arches =
   let aId = nextArchetypeId arches
@@ -101,20 +128,30 @@
           }
       )
 
+-- | Adjust an `Archetype` by its `ArchetypeID`.
+--
+-- @since 9.0
 adjustArchetype :: ArchetypeID -> (Archetype -> Archetype) -> Archetypes -> Archetypes
-adjustArchetype aId f arches = arches {nodes = Map.adjust (\node -> node {nodeArchetype = f (nodeArchetype node)}) aId (nodes arches)}
+adjustArchetype aId f arches =
+  arches {nodes = Map.adjust (\node -> node {nodeArchetype = f (nodeArchetype node)}) aId (nodes arches)}
 
 -- | Find `ArchetypeID`s containing a set of `ComponentID`s.
+--
+-- @since 9.0
 findArchetypeIds :: Set ComponentID -> Archetypes -> Set ArchetypeID
 findArchetypeIds cIds arches = case mapMaybe (\cId -> Map.lookup cId (componentIds arches)) (Set.elems cIds) of
   (aId : aIds') -> foldl' Set.intersection aId aIds'
   [] -> Set.empty
 
 -- | Lookup `Archetype`s containing a set of `ComponentID`s.
+--
+-- @since 9.0
 find :: Set ComponentID -> Archetypes -> Map ArchetypeID Node
 find cIds arches = Map.fromSet (\aId -> nodes arches Map.! aId) (findArchetypeIds cIds arches)
 
 -- | Map over `Archetype`s containing a set of `ComponentID`s.
+--
+-- @since 9.0
 map :: Set ComponentID -> (Archetype -> (a, Archetype)) -> Archetypes -> ([a], Archetypes)
 map cIds f arches =
   let go (acc, archAcc) aId =
@@ -124,57 +161,58 @@
          in (a : acc, archAcc {nodes = nodes'})
    in foldl' go ([], arches) $ findArchetypeIds cIds arches
 
+-- | Lookup an `ArchetypeID` by its set of `ComponentID`s.
+--
+-- @since 9.0
 lookupArchetypeId :: Set ComponentID -> Archetypes -> Maybe ArchetypeID
 lookupArchetypeId cIds arches = Map.lookup cIds (archetypeIds arches)
 
+-- | Lookup an `Archetype` by its `ArchetypeID`.
+--
+-- @since 9.0
 lookup :: ArchetypeID -> Archetypes -> Maybe Node
 lookup aId arches = Map.lookup aId (nodes arches)
 
 -- | Insert a component into an entity with its `ComponentID`.
+--
+-- @since 9.0
 insert ::
-  (Component a) =>
   EntityID ->
   ArchetypeID ->
-  ComponentID ->
-  a ->
+  Set ComponentID ->
+  DynamicBundle ->
   Archetypes ->
   (Maybe ArchetypeID, Archetypes)
-insert e aId cId c arches = case lookup aId arches of
+insert e aId cIds b arches = case lookup aId arches of
   Just node ->
-    if Set.member cId (nodeComponentIds node)
+    if cIds == nodeComponentIds node
       then
-        let go n = n {nodeArchetype = insertComponent e cId c (nodeArchetype n)}
-         in (Nothing, arches {nodes = Map.adjust go aId (nodes arches)})
-      else case lookupArchetypeId (Set.insert cId (nodeComponentIds node)) arches of
+        let go n = n {nodeArchetype = runDynamicBundle b e $ nodeArchetype n}
+         in (Nothing, arches {nodes = Map.adjust go aId $ nodes arches})
+      else case lookupArchetypeId cIds arches of
         Just nextAId ->
-          let !(cs, arch') = A.remove e (nodeArchetype node)
+          let !(cs, arch') = A.remove e $ nodeArchetype node
               node' = node {nodeArchetype = arch'}
               !arches' = arches {nodes = Map.insert aId node' (nodes arches)}
-              f archAcc (itemCId, dyn) =
-                let storages' = Map.adjust go itemCId (storages archAcc)
-                    go s = fromAscListDyn (Map.elems . Map.insert e dyn . Map.fromAscList . zip (Set.toList $ entities archAcc) $ toAscListDyn s) s
-                 in archAcc {storages = storages'}
               adjustNode nextNode =
-                let nextArch = foldl' f (nodeArchetype nextNode) (Map.toList cs)
-                 in nextNode {nodeArchetype = insertComponent e cId c nextArch}
+                let !nextArch = A.insertComponents e cs $ nodeArchetype nextNode
+                 in nextNode {nodeArchetype = runDynamicBundle b e nextArch}
            in (Just nextAId, arches' {nodes = Map.adjust adjustNode nextAId (nodes arches')})
         Nothing ->
-          let !(s, arch') = removeStorages e (nodeArchetype node)
+          let !(s, arch') = A.removeStorages e $ nodeArchetype node
               !n =
                 Node
-                  { nodeComponentIds = Set.insert cId (nodeComponentIds node),
-                    nodeArchetype = insertComponent e cId c (Archetype {storages = s, entities = Set.singleton e}),
-                    nodeAdd = Map.empty,
-                    nodeRemove = Map.singleton cId aId
+                  { nodeComponentIds = cIds,
+                    nodeArchetype = runDynamicBundle b e (Archetype {storages = s, entities = Set.singleton e})
                   }
-              cIds = Set.insert cId (nodeComponentIds node)
               !(nextAId, arches') = insertArchetype cIds n arches
-           in let node' =
-                    node {nodeArchetype = arch', nodeAdd = Map.insert cId nextAId (nodeAdd node)}
-                  nodes' = Map.insert aId node' (nodes arches')
+           in let nodes' = Map.insert aId (node {nodeArchetype = arch'}) (nodes arches')
                in (Just nextAId, arches' {nodes = nodes'})
   Nothing -> (Nothing, arches)
 
+-- | Remove a component from an entity with its `ComponentID`.
+--
+-- @since 9.0
 remove ::
   (Component a) =>
   EntityID ->
@@ -187,27 +225,25 @@
     Just nextAId ->
       let !(cs, arch') = A.remove e (nodeArchetype node)
           !arches' = arches {nodes = Map.insert aId node {nodeArchetype = arch'} (nodes arches)}
-          (a, cs') = Map.updateLookupWithKey (\_ _ -> Nothing) cId cs
+          (a, cs') = IntMap.updateLookupWithKey (\_ _ -> Nothing) (unComponentId cId) cs
           go' archAcc (itemCId, dyn) =
             let adjustStorage s = fromAscListDyn (Map.elems . Map.insert e dyn . Map.fromAscList . zip (Set.toList $ entities archAcc) $ toAscListDyn s) s
-             in archAcc {storages = Map.adjust adjustStorage itemCId (storages archAcc)}
+             in archAcc {storages = IntMap.adjust adjustStorage itemCId (storages archAcc)}
           go nextNode =
-            nextNode {nodeArchetype = foldl' go' (nodeArchetype nextNode) (Map.toList cs')}
+            nextNode {nodeArchetype = foldl' go' (nodeArchetype nextNode) (IntMap.toList cs')}
        in ( (,nextAId) <$> (a >>= fromDynamic),
             arches' {nodes = Map.adjust go nextAId (nodes arches')}
           )
     Nothing ->
-      let !(cs, arch') = removeStorages e (nodeArchetype node)
-          (a, cs') = Map.updateLookupWithKey (\_ _ -> Nothing) cId cs
+      let !(cs, arch') = A.removeStorages e (nodeArchetype node)
+          (a, cs') = IntMap.updateLookupWithKey (\_ _ -> Nothing) (unComponentId cId) cs
           !n =
             Node
               { nodeComponentIds = Set.insert cId (nodeComponentIds node),
-                nodeArchetype = Archetype {storages = cs', entities = Set.singleton e},
-                nodeAdd = Map.empty,
-                nodeRemove = Map.singleton cId aId
+                nodeArchetype = Archetype {storages = cs', entities = Set.singleton e}
               }
           !(nextAId, arches') = insertArchetype (Set.insert cId (nodeComponentIds node)) n arches
-          node' = node {nodeArchetype = arch', nodeAdd = Map.insert cId nextAId (nodeAdd node)}
+          node' = node {nodeArchetype = arch'}
           removeDyn s =
             let (res, dyns) = Map.updateLookupWithKey (\_ _ -> Nothing) e . Map.fromAscList . zip (Set.toList $ entities arch') $ toAscListDyn s
              in (res, fromAscListDyn $ Map.elems dyns)
diff --git a/src/Aztecs/ECS/World/Bundle.hs b/src/Aztecs/ECS/World/Bundle.hs
--- a/src/Aztecs/ECS/World/Bundle.hs
+++ b/src/Aztecs/ECS/World/Bundle.hs
@@ -3,9 +3,18 @@
 {-# LANGUAGE InstanceSigs #-}
 {-# LANGUAGE RankNTypes #-}
 {-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TupleSections #-}
 {-# LANGUAGE TypeApplications #-}
 {-# LANGUAGE TypeFamilies #-}
 
+-- |
+-- Module      : Aztecs.ECS.World.Bundle
+-- Copyright   : (c) Matt Hunzinger, 2025
+-- License     : BSD-style (see the LICENSE file in the distribution)
+--
+-- Maintainer  : matt@hunzinger.me
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
 module Aztecs.ECS.World.Bundle
   ( Bundle (..),
     MonoidBundle (..),
@@ -14,37 +23,50 @@
   )
 where
 
-import Aztecs.ECS.Component (Component (..), ComponentID)
-import Aztecs.ECS.Entity (EntityID)
-import Aztecs.ECS.World.Archetype (Archetype)
+import Aztecs.ECS.Component
+import Aztecs.ECS.Entity
+import Aztecs.ECS.World.Archetype
 import Aztecs.ECS.World.Bundle.Class
 import Aztecs.ECS.World.Bundle.Dynamic
-import Aztecs.ECS.World.Components (Components)
+import Aztecs.ECS.World.Components
 import qualified Aztecs.ECS.World.Components as CS
 import Data.Set (Set)
 import qualified Data.Set as Set
 
 -- | Bundle of components.
-newtype Bundle = Bundle {unBundle :: Components -> (Set ComponentID, Components, DynamicBundle)}
+--
+-- @since 9.0
+newtype Bundle = Bundle
+  { -- | Unwrap the bundle.
+    --
+    -- @since 9.0
+    unBundle :: Components -> (Set ComponentID, Components, DynamicBundle)
+  }
 
+-- | @since 9.0
 instance Monoid Bundle where
-  mempty = Bundle $ \cs -> (Set.empty, cs, mempty)
+  mempty = Bundle (Set.empty,,mempty)
 
+-- | @since 9.0
 instance Semigroup Bundle where
   Bundle b1 <> Bundle b2 = Bundle $ \cs ->
     let (cIds1, cs', d1) = b1 cs
         (cIds2, cs'', d2) = b2 cs'
      in (cIds1 <> cIds2, cs'', d1 <> d2)
 
+-- | @since 9.0
 instance MonoidBundle Bundle where
   bundle :: forall a. (Component a) => a -> Bundle
   bundle a = Bundle $ \cs ->
     let (cId, cs') = CS.insert @a cs in (Set.singleton cId, cs', dynBundle cId a)
 
+-- | @since 9.0
 instance MonoidDynamicBundle Bundle where
-  dynBundle cId c = Bundle $ \cs -> (Set.singleton cId, cs, dynBundle cId c)
+  dynBundle cId c = Bundle (Set.singleton cId,,dynBundle cId c)
 
 -- | Insert a bundle of components into an archetype.
+--
+-- @since 9.0
 runBundle :: Bundle -> Components -> EntityID -> Archetype -> (Components, Archetype)
 runBundle b cs eId arch =
   let !(_, cs', d) = unBundle b cs
diff --git a/src/Aztecs/ECS/World/Bundle/Class.hs b/src/Aztecs/ECS/World/Bundle/Class.hs
--- a/src/Aztecs/ECS/World/Bundle/Class.hs
+++ b/src/Aztecs/ECS/World/Bundle/Class.hs
@@ -1,11 +1,23 @@
 {-# LANGUAGE FlexibleContexts #-}
 {-# LANGUAGE RankNTypes #-}
 
+-- |
+-- Module      : Aztecs.ECS.World.Bundle.Class
+-- Copyright   : (c) Matt Hunzinger, 2025
+-- License     : BSD-style (see the LICENSE file in the distribution)
+--
+-- Maintainer  : matt@hunzinger.me
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
 module Aztecs.ECS.World.Bundle.Class (MonoidBundle (..)) where
 
-import Aztecs.ECS.Component (Component (..))
+import Aztecs.ECS.Component
 
 -- | Monoid bundle of components.
+--
+-- @since 9.0
 class (Monoid a) => MonoidBundle a where
   -- | Add a component to the bundle.
+  --
+  -- @since 9.0
   bundle :: forall c. (Component c) => c -> a
diff --git a/src/Aztecs/ECS/World/Bundle/Dynamic.hs b/src/Aztecs/ECS/World/Bundle/Dynamic.hs
--- a/src/Aztecs/ECS/World/Bundle/Dynamic.hs
+++ b/src/Aztecs/ECS/World/Bundle/Dynamic.hs
@@ -1,21 +1,30 @@
-module Aztecs.ECS.World.Bundle.Dynamic
-  ( DynamicBundle (..),
-    MonoidDynamicBundle (..),
-  )
-where
+-- |
+-- Module      : Aztecs.ECS.World.Bundle.Dynamic
+-- Copyright   : (c) Matt Hunzinger, 2025
+-- License     : BSD-style (see the LICENSE file in the distribution)
+--
+-- Maintainer  : matt@hunzinger.me
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
+module Aztecs.ECS.World.Bundle.Dynamic (DynamicBundle (..), MonoidDynamicBundle (..)) where
 
-import Aztecs.ECS.Entity (EntityID)
-import Aztecs.ECS.World.Archetype (Archetype, insertComponent)
-import Aztecs.ECS.World.Bundle.Dynamic.Class (MonoidDynamicBundle (..))
+import Aztecs.ECS.Entity
+import Aztecs.ECS.World.Archetype
+import Aztecs.ECS.World.Bundle.Dynamic.Class
 
 -- | Dynamic bundle of components.
+--
+-- @since 9.0
 newtype DynamicBundle = DynamicBundle {runDynamicBundle :: EntityID -> Archetype -> Archetype}
 
+-- | @since 9.0
 instance Semigroup DynamicBundle where
   DynamicBundle d1 <> DynamicBundle d2 = DynamicBundle $ \eId arch -> d2 eId (d1 eId arch)
 
+-- | @since 9.0
 instance Monoid DynamicBundle where
   mempty = DynamicBundle $ \_ arch -> arch
 
+-- | @since 9.0
 instance MonoidDynamicBundle DynamicBundle where
   dynBundle cId a = DynamicBundle $ \eId arch -> insertComponent eId cId a arch
diff --git a/src/Aztecs/ECS/World/Bundle/Dynamic/Class.hs b/src/Aztecs/ECS/World/Bundle/Dynamic/Class.hs
--- a/src/Aztecs/ECS/World/Bundle/Dynamic/Class.hs
+++ b/src/Aztecs/ECS/World/Bundle/Dynamic/Class.hs
@@ -1,9 +1,20 @@
+-- |
+-- Module      : Aztecs.ECS.World.Bundle.Dynamic.Class
+-- Copyright   : (c) Matt Hunzinger, 2025
+-- License     : BSD-style (see the LICENSE file in the distribution)
+--
+-- Maintainer  : matt@hunzinger.me
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
 module Aztecs.ECS.World.Bundle.Dynamic.Class (MonoidDynamicBundle (..)) where
 
-import Aztecs.ECS.Component (Component (..))
-import Aztecs.ECS.World.Components (ComponentID)
+import Aztecs.ECS.Component
 
 -- | Monoid bundle of dynamic components.
+--
+-- @since 9.0
 class MonoidDynamicBundle a where
   -- | Add a component to the bundle by its `ComponentID`.
+  --
+  -- @since 9.0
   dynBundle :: (Component c) => ComponentID -> c -> a
diff --git a/src/Aztecs/ECS/World/Components.hs b/src/Aztecs/ECS/World/Components.hs
--- a/src/Aztecs/ECS/World/Components.hs
+++ b/src/Aztecs/ECS/World/Components.hs
@@ -5,6 +5,14 @@
 {-# LANGUAGE ScopedTypeVariables #-}
 {-# LANGUAGE TypeApplications #-}
 
+-- |
+-- Module      : Aztecs.ECS.World.Components
+-- Copyright   : (c) Matt Hunzinger, 2025
+-- License     : BSD-style (see the LICENSE file in the distribution)
+--
+-- Maintainer  : matt@hunzinger.me
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
 module Aztecs.ECS.World.Components
   ( ComponentID (..),
     Components (..),
@@ -15,22 +23,32 @@
   )
 where
 
-import Aztecs.ECS.Component (Component, ComponentID (..))
-import Control.DeepSeq (NFData)
+import Aztecs.ECS.Component
+import Control.DeepSeq
 import Data.Map.Strict (Map)
 import qualified Data.Map.Strict as Map
-import Data.Typeable (Proxy (..), TypeRep, Typeable, typeOf)
-import GHC.Generics (Generic)
+import Data.Typeable
+import GHC.Generics
 import Prelude hiding (lookup)
 
 -- | Component ID map.
+--
+-- @since 9.0
 data Components = Components
-  { componentIds :: !(Map TypeRep ComponentID),
+  { -- | Map of component types to identifiers.
+    --
+    -- @since 9.0
+    componentIds :: !(Map TypeRep ComponentID),
+    -- | Next unique component identifier.
+    --
+    -- @since 9.0
     nextComponentId :: !ComponentID
   }
   deriving (Show, Generic, NFData)
 
 -- | Empty `Components`.
+--
+-- @since 9.0
 empty :: Components
 empty =
   Components
@@ -39,16 +57,22 @@
     }
 
 -- | Lookup a component ID by type.
+--
+-- @since 9.0
 lookup :: forall a. (Typeable a) => Components -> Maybe ComponentID
 lookup cs = Map.lookup (typeOf (Proxy @a)) (componentIds cs)
 
 -- | Insert a component ID by type, if it does not already exist.
+--
+-- @since 9.0
 insert :: forall a. (Component a) => Components -> (ComponentID, Components)
 insert cs = case lookup @a cs of
   Just cId -> (cId, cs)
   Nothing -> insert' @a cs
 
 -- | Insert a component ID by type.
+--
+-- @since 9.0
 insert' :: forall c. (Component c) => Components -> (ComponentID, Components)
 insert' cs =
   let !cId = nextComponentId cs
diff --git a/src/Aztecs/ECS/World/Entities.hs b/src/Aztecs/ECS/World/Entities.hs
--- a/src/Aztecs/ECS/World/Entities.hs
+++ b/src/Aztecs/ECS/World/Entities.hs
@@ -5,15 +5,21 @@
 {-# LANGUAGE TypeApplications #-}
 {-# LANGUAGE TypeFamilies #-}
 
+-- |
+-- Module      : Aztecs.ECS.World.Entities
+-- Copyright   : (c) Matt Hunzinger, 2025
+-- License     : BSD-style (see the LICENSE file in the distribution)
+--
+-- Maintainer  : matt@hunzinger.me
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
 module Aztecs.ECS.World.Entities
   ( Entities (..),
     empty,
     spawn,
-    spawnComponent,
-    spawnWithId,
     spawnWithArchetypeId,
     insert,
-    insertWithId,
+    insertDyn,
     lookup,
     remove,
     removeWithId,
@@ -32,23 +38,38 @@
 import qualified Aztecs.ECS.World.Components as CS
 import Control.DeepSeq
 import Data.Dynamic
+import Data.IntMap (IntMap)
+import qualified Data.IntMap as IntMap
 import Data.Map (Map)
 import qualified Data.Map as Map
 import Data.Maybe
+import Data.Set (Set)
 import qualified Data.Set as Set
-import Data.Typeable
 import GHC.Generics
 import Prelude hiding (lookup)
 
 -- | World of entities and their components.
+--
+-- @since 9.0
 data Entities = Entities
-  { archetypes :: !Archetypes,
+  { -- | Archetypes.
+    --
+    -- @since 9.0
+    archetypes :: !Archetypes,
+    -- | Components.
+    --
+    -- @since 9.0
     components :: !Components,
+    -- | Entities and their archetype identifiers.
+    --
+    -- @since 9.0
     entities :: !(Map EntityID ArchetypeID)
   }
   deriving (Show, Generic, NFData)
 
 -- | Empty `World`.
+--
+-- @since 9.0
 empty :: Entities
 empty =
   Entities
@@ -57,6 +78,9 @@
       entities = mempty
     }
 
+-- | Spawn a `Bundle`.
+--
+-- @since 9.0
 spawn :: EntityID -> Bundle -> Entities -> Entities
 spawn eId b w =
   let (cIds, components', dynB) = unBundle b (components w)
@@ -78,103 +102,67 @@
                 entities = Map.insert eId aId (entities w)
               }
         Nothing ->
-          let arch' = runDynamicBundle dynB eId A.empty {A.entities = Set.singleton eId}
-              (aId, arches) =
-                AS.insertArchetype
-                  cIds
-                  ( Node
-                      { nodeComponentIds = cIds,
-                        nodeArchetype = arch',
-                        nodeAdd = Map.empty,
-                        nodeRemove = Map.empty
-                      }
-                  )
-                  (archetypes w)
+          let arch' = runDynamicBundle dynB eId $ A.singleton eId
+              node' = Node {nodeComponentIds = cIds, nodeArchetype = arch'}
+              (aId, arches) = AS.insertArchetype cIds node' $ archetypes w
            in w
                 { archetypes = arches,
                   entities = Map.insert eId aId (entities w),
                   components = components'
                 }
 
--- | Spawn an entity with a component.
-spawnComponent :: forall a. (Component a) => EntityID -> a -> Entities -> Entities
-spawnComponent e c w = case Map.lookup (typeOf (Proxy @a)) (componentIds (components w)) of
-  Just cId -> spawnWithId e cId c w
-  Nothing ->
-    let (cId, cs) = CS.insert @a (components w) in spawnWithId e cId c w {components = cs}
-
--- | Spawn an entity with a component and its `ComponentID`.
-spawnWithId ::
-  forall a.
-  (Component a) =>
-  EntityID ->
-  ComponentID ->
-  a ->
-  Entities ->
-  Entities
-spawnWithId e cId c w = case AS.lookupArchetypeId (Set.singleton cId) (archetypes w) of
-  Just aId -> spawnWithArchetypeId e aId cId c w
-  Nothing ->
-    let !(aId, arches) =
-          AS.insertArchetype
-            (Set.singleton cId)
-            ( Node
-                { nodeComponentIds = Set.singleton cId,
-                  nodeArchetype = A.insertComponent e cId c A.empty,
-                  nodeAdd = Map.empty,
-                  nodeRemove = Map.empty
-                }
-            )
-            (archetypes w)
-     in w {archetypes = arches, entities = Map.insert e aId (entities w)}
-
+-- | Spawn a `DynamicBundle` with a specified `ArchetypeID`.
+--
+-- @since 9.0
 spawnWithArchetypeId ::
-  forall a.
-  (Component a) =>
   EntityID ->
   ArchetypeID ->
-  ComponentID ->
-  a ->
+  DynamicBundle ->
   Entities ->
   Entities
-spawnWithArchetypeId e aId cId c w =
-  let f n = n {nodeArchetype = A.insertComponent e cId c (nodeArchetype n)}
+spawnWithArchetypeId e aId b w =
+  let f n = n {nodeArchetype = runDynamicBundle b e ((nodeArchetype n) {A.entities = Set.insert e . A.entities $ nodeArchetype n})}
    in w
         { archetypes = (archetypes w) {AS.nodes = Map.adjust f aId (AS.nodes $ archetypes w)},
           entities = Map.insert e aId (entities w)
         }
 
 -- | Insert a component into an entity.
-insert :: forall a. (Component a) => EntityID -> a -> Entities -> Entities
-insert e c w =
-  let !(cId, components') = CS.insert @a (components w)
-   in insertWithId e cId c w {components = components'}
+--
+-- @since 9.0
+insert :: EntityID -> Bundle -> Entities -> Entities
+insert e b w =
+  let !(cIds, components', dynB) = unBundle b (components w)
+   in insertDyn e cIds dynB w {components = components'}
 
 -- | Insert a component into an entity with its `ComponentID`.
-insertWithId :: (Component a) => EntityID -> ComponentID -> a -> Entities -> Entities
-insertWithId e cId c w = case Map.lookup e (entities w) of
+--
+-- @since 9.0
+insertDyn :: EntityID -> Set ComponentID -> DynamicBundle -> Entities -> Entities
+insertDyn e cIds b w = case Map.lookup e (entities w) of
   Just aId ->
-    let (maybeNextAId, arches) = AS.insert e aId cId c $ archetypes w
+    let (maybeNextAId, arches) = AS.insert e aId cIds b $ archetypes w
         es = case maybeNextAId of
           Just nextAId -> Map.insert e nextAId (entities w)
           Nothing -> entities w
      in w {archetypes = arches, entities = es}
-  Nothing -> case AS.lookupArchetypeId (Set.singleton cId) (archetypes w) of
-    Just aId -> spawnWithArchetypeId e aId cId c w
+  Nothing -> case AS.lookupArchetypeId cIds (archetypes w) of
+    Just aId -> spawnWithArchetypeId e aId b w
     Nothing ->
       let (aId, arches) =
             AS.insertArchetype
-              (Set.singleton cId)
+              cIds
               ( Node
-                  { nodeComponentIds = Set.singleton cId,
-                    nodeArchetype = A.insertComponent e cId c A.empty,
-                    nodeAdd = Map.empty,
-                    nodeRemove = Map.empty
+                  { nodeComponentIds = cIds,
+                    nodeArchetype = runDynamicBundle b e $ A.singleton e
                   }
               )
               (archetypes w)
        in w {archetypes = arches, entities = Map.insert e aId (entities w)}
 
+-- | Lookup a component in an entity.
+--
+-- @since 9.0
 lookup :: forall a. (Component a) => EntityID -> Entities -> Maybe a
 lookup e w = do
   !cId <- CS.lookup @a $ components w
@@ -183,11 +171,16 @@
   A.lookupComponent e cId $ nodeArchetype node
 
 -- | Insert a component into an entity.
+--
+-- @since 9.0
 remove :: forall a. (Component a) => EntityID -> Entities -> (Maybe a, Entities)
 remove e w =
   let !(cId, components') = CS.insert @a (components w)
    in removeWithId @a e cId w {components = components'}
 
+-- | Remove a component from an entity with its `ComponentID`.
+--
+-- @since 9.0
 removeWithId :: forall a. (Component a) => EntityID -> ComponentID -> Entities -> (Maybe a, Entities)
 removeWithId e cId w = case Map.lookup e (entities w) of
   Just aId ->
@@ -199,7 +192,9 @@
   Nothing -> (Nothing, w)
 
 -- | Despawn an entity, returning its components.
-despawn :: EntityID -> Entities -> (Map ComponentID Dynamic, Entities)
+--
+-- @since 9.0
+despawn :: EntityID -> Entities -> (IntMap Dynamic, Entities)
 despawn e w =
   let res = do
         !aId <- Map.lookup e $ entities w
@@ -214,4 +209,4 @@
                     entities = Map.delete e (entities w)
                   }
               )
-        Nothing -> (Map.empty, w)
+        Nothing -> (IntMap.empty, w)
diff --git a/src/Aztecs/ECS/World/Storage.hs b/src/Aztecs/ECS/World/Storage.hs
--- a/src/Aztecs/ECS/World/Storage.hs
+++ b/src/Aztecs/ECS/World/Storage.hs
@@ -1,23 +1,78 @@
 {-# LANGUAGE FlexibleInstances #-}
 {-# LANGUAGE MultiParamTypeClasses #-}
 
+-- |
+-- Module      : Aztecs.ECS.World.Storage
+-- Copyright   : (c) Matt Hunzinger, 2025
+-- License     : BSD-style (see the LICENSE file in the distribution)
+--
+-- Maintainer  : matt@hunzinger.me
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
 module Aztecs.ECS.World.Storage (Storage (..)) where
 
 import Control.DeepSeq
+import qualified Control.Monad
 import Data.Data
+import Prelude hiding (zipWith)
+import qualified Prelude
 
 -- | Component storage, containing zero or many components of the same type.
+--
+-- @since 9.0
 class (Typeable s, NFData s, Typeable a) => Storage a s where
   -- | Storage with a single component.
+  --
+  -- @since 9.0
   singleton :: a -> s
 
   -- | List of all components in the storage in ascending order.
+  --
+  -- @since 9.0
   toAscList :: s -> [a]
 
   -- | Convert a sorted list of components (in ascending order) into a storage.
+  --
+  -- @since 9.0
   fromAscList :: [a] -> s
 
+  -- | Map a function over all components in the storage.
+  --
+  --
+  -- @since 9.0
+  map :: (a -> a) -> s -> s
+
+  -- | Map a function with some input over all components in the storage.
+  --
+  -- @since 9.0
+  zipWith :: (i -> a -> a) -> [i] -> s -> ([a], s)
+
+  -- | Map a monadic function with some input over all components in the storage.
+  --
+  -- @since 9.0
+  zipWithM :: (Monad m) => (i -> a -> m a) -> [i] -> s -> m ([a], s)
+
+  -- | Map a function with some input over all components in the storage.
+  --
+  -- @since 9.0
+  zipWith_ :: (i -> a -> a) -> [i] -> s -> s
+  zipWith_ f is as = snd $ zipWith f is as
+
+-- | @since 9.0
 instance (Typeable a, NFData a) => Storage a [a] where
+  {-# INLINE singleton #-}
   singleton a = [a]
+  {-# INLINE toAscList #-}
   toAscList = id
+  {-# INLINE fromAscList #-}
   fromAscList = id
+  {-# INLINE map #-}
+  map = fmap
+  {-# INLINE zipWith #-}
+  zipWith f is as = let as' = Prelude.zipWith f is as in (as', as')
+  {-# INLINE zipWith_ #-}
+  zipWith_ = Prelude.zipWith
+  {-# INLINE zipWithM #-}
+  zipWithM f is as = do
+    as' <- Control.Monad.zipWithM f is as
+    return (as', as')
diff --git a/src/Aztecs/ECS/World/Storage/Dynamic.hs b/src/Aztecs/ECS/World/Storage/Dynamic.hs
--- a/src/Aztecs/ECS/World/Storage/Dynamic.hs
+++ b/src/Aztecs/ECS/World/Storage/Dynamic.hs
@@ -8,6 +8,14 @@
 {-# LANGUAGE TypeApplications #-}
 {-# LANGUAGE TypeFamilies #-}
 
+-- |
+-- Module      : Aztecs.ECS.World.Storage.Dynamic
+-- Copyright   : (c) Matt Hunzinger, 2025
+-- License     : BSD-style (see the LICENSE file in the distribution)
+--
+-- Maintainer  : matt@hunzinger.me
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
 module Aztecs.ECS.World.Storage.Dynamic
   ( DynamicStorage (..),
     dynStorage,
@@ -22,20 +30,44 @@
 import Data.Dynamic
 import Data.Maybe
 
+-- | Dynamic storage of components.
+--
+-- @since 9.0
 data DynamicStorage = DynamicStorage
-  { storageDyn :: !Dynamic,
+  { -- | Dynamic storage.
+    --
+    -- @since 9.0
+    storageDyn :: !Dynamic,
+    -- | Singleton storage.
+    --
+    -- @since 9.0
     singletonDyn' :: !(Dynamic -> Dynamic),
+    -- | Convert this storage to an ascending list.
+    --
+    -- @since 9.0
     toAscListDyn' :: !(Dynamic -> [Dynamic]),
+    -- | Convert from an ascending list.
+    --
+    -- @since 9.0
     fromAscListDyn' :: !([Dynamic] -> Dynamic),
+    -- | Reduce this storage to normal form.
+    --
+    -- @since 9.0
     storageRnf :: !(Dynamic -> ())
   }
 
+-- | @since 9.0
 instance Show DynamicStorage where
   show s = "DynamicStorage " ++ show (storageDyn s)
 
+-- | @since 9.0
 instance NFData DynamicStorage where
   rnf s = storageRnf s (storageDyn s)
 
+-- | Create a dynamic storage from a storage.
+--
+-- @since 9.0
+{-# INLINE dynStorage #-}
 dynStorage :: forall a s. (S.Storage a s) => s -> DynamicStorage
 dynStorage s =
   DynamicStorage
@@ -46,11 +78,20 @@
       storageRnf = maybe () rnf . fromDynamic @s
     }
 
+-- | Singleton dynamic storage.
+--
+-- @since 9.0
 singletonDyn :: Dynamic -> DynamicStorage -> DynamicStorage
 singletonDyn dyn s = s {storageDyn = singletonDyn' s dyn}
 
+-- | Convert from an ascending list.
+--
+-- @since 9.0
 fromAscListDyn :: [Dynamic] -> DynamicStorage -> DynamicStorage
 fromAscListDyn dyns s = s {storageDyn = fromAscListDyn' s dyns}
 
+-- | Convert this storage to an ascending list.
+--
+-- @since 9.0
 toAscListDyn :: DynamicStorage -> [Dynamic]
 toAscListDyn = toAscListDyn' <*> storageDyn
diff --git a/src/Aztecs/Hierarchy.hs b/src/Aztecs/Hierarchy.hs
--- a/src/Aztecs/Hierarchy.hs
+++ b/src/Aztecs/Hierarchy.hs
@@ -5,6 +5,17 @@
 {-# LANGUAGE GeneralizedNewtypeDeriving #-}
 {-# LANGUAGE TypeApplications #-}
 
+-- |
+-- Module      : Aztecs.Asset.AssetServer
+-- Copyright   : (c) Matt Hunzinger, 2025
+-- License     : BSD-style (see the LICENSE file in the distribution)
+--
+-- Maintainer  : matt@hunzinger.me
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
+--
+-- Hierarchical relationships.
+-- A `Children` component forms a one-to-many relationship with `Parent` components.
 module Aztecs.Hierarchy
   ( Parent (..),
     Children (..),
@@ -25,172 +36,226 @@
 import qualified Aztecs.ECS.Access as A
 import qualified Aztecs.ECS.Query as Q
 import qualified Aztecs.ECS.System as S
-import Control.Arrow (returnA)
+import Control.Arrow
 import Control.DeepSeq
-import Control.Monad (when)
+import Control.Monad
 import Data.Map (Map)
 import qualified Data.Map as Map
-import Data.Maybe (mapMaybe)
+import Data.Maybe
 import Data.Set (Set)
 import qualified Data.Set as Set
-import GHC.Generics (Generic)
+import GHC.Generics
 
-newtype Parent = Parent {unParent :: EntityID}
+-- | Parent component.
+--
+-- @since 9.0
+newtype Parent = Parent
+  { -- | Parent entity ID.
+    --
+    -- @since 9.0
+    unParent :: EntityID
+  }
   deriving (Eq, Ord, Show, Generic, NFData)
 
+-- | @since 9.0
 instance Component Parent
 
+-- | Parent internal state component.
+--
+-- @since 9.0
 newtype ParentState = ParentState {unParentState :: EntityID}
   deriving (Show, Generic, NFData)
 
+-- | @since 9.0
 instance Component ParentState
 
+-- | Children component.
+--
+-- @since 9.0
 newtype Children = Children {unChildren :: Set EntityID}
   deriving (Eq, Ord, Show, Semigroup, Monoid, Generic, NFData)
 
+-- | @since 9.0
 instance Component Children
 
+-- | Child internal state component.
 newtype ChildState = ChildState {unChildState :: Set EntityID}
   deriving (Show, Generic, NFData)
 
+-- | @since 9.0
 instance Component ChildState
 
+-- | Update the parent-child relationships.
+--
+-- @since 9.0
 update ::
   ( ArrowQueryReader qr,
     ArrowDynamicQueryReader qr,
-    ArrowReaderSystem qr arr,
-    ArrowQueueSystem b m arr
+    MonadReaderSystem qr s,
+    MonadAccess b m
   ) =>
-  arr () ()
-update = proc () -> do
+  s (m ())
+update = do
   parents <-
     S.all
+      ()
       ( proc () -> do
           entity <- Q.entity -< ()
           Parent parent <- Q.fetch -< ()
           maybeParentState <- Q.fetchMaybe @_ @ParentState -< ()
           returnA -< (entity, parent, maybeParentState)
       )
-      -<
-        ()
+
   children <-
     S.all
+      ()
       ( proc () -> do
           entity <- Q.entity -< ()
           Children cs <- Q.fetch -< ()
           maybeChildState <- Q.fetchMaybe @_ @ChildState -< ()
           returnA -< (entity, cs, maybeChildState)
       )
-      -<
-        ()
-  S.queue
-    ( \(parents, childRes) -> do
+
+  let go = do
         mapM_
           ( \(entity, parent, maybeParentState) -> case maybeParentState of
               Just (ParentState parentState) -> do
                 when (parent /= parentState) $ do
-                  A.insert parent $ ParentState parent
+                  A.insert parent . bundle $ ParentState parent
 
                   -- Remove this entity from the previous parent's children.
                   maybeLastChildren <- A.lookup parentState
                   let lastChildren = maybe mempty unChildren maybeLastChildren
                   let lastChildren' = Set.filter (/= entity) lastChildren
-                  A.insert parentState . Children $ lastChildren'
+                  A.insert parentState . bundle . Children $ lastChildren'
 
                   -- Add this entity to the new parent's children.
                   maybeChildren <- A.lookup parent
                   let parentChildren = maybe mempty unChildren maybeChildren
-                  A.insert parent . Children $ Set.insert entity parentChildren
+                  A.insert parent . bundle . Children $ Set.insert entity parentChildren
               Nothing -> do
                 A.spawn_ . bundle $ ParentState parent
                 maybeChildren <- A.lookup parent
                 let parentChildren = maybe mempty unChildren maybeChildren
-                A.insert parent . Children $ Set.insert entity parentChildren
+                A.insert parent . bundle . Children $ Set.insert entity parentChildren
           )
           parents
         mapM_
-          ( \(entity, children, maybeChildState) -> case maybeChildState of
+          ( \(entity, children', maybeChildState) -> case maybeChildState of
               Just (ChildState childState) -> do
-                when (children /= childState) $ do
-                  A.insert entity $ ChildState children
-                  let added = Set.difference children childState
-                      removed = Set.difference childState children
-                  mapM_ (\e -> A.insert e . Parent $ entity) added
+                when (children' /= childState) $ do
+                  A.insert entity . bundle $ ChildState children'
+                  let added = Set.difference children' childState
+                      removed = Set.difference childState children'
+                  mapM_ (\e -> A.insert e . bundle . Parent $ entity) added
                   mapM_ (A.remove @_ @_ @Parent) removed
               Nothing -> do
-                A.insert entity $ ChildState children
-                mapM_ (\e -> A.insert e . Parent $ entity) children
+                A.insert entity . bundle $ ChildState children'
+                mapM_ (\e -> A.insert e . bundle . Parent $ entity) children'
           )
-          childRes
-    )
-    -<
-      (parents, children)
+          children
+  return go
 
+-- | Hierarchy of entities.
+--
+-- @since 9.0
 data Hierarchy a = Node
-  { nodeEntityId :: EntityID,
+  { -- | Entity ID.
+    --
+    -- @since 9.0
+    nodeEntityId :: EntityID,
+    -- | Entity components.
     nodeEntity :: a,
+    -- | Child nodes.
+    --
+    -- @since 9.0
     nodeChildren :: [Hierarchy a]
   }
   deriving (Functor)
 
+-- | @since 9.0
 instance Foldable Hierarchy where
   foldMap f n = f (nodeEntity n) <> foldMap (foldMap f) (nodeChildren n)
 
+-- | @since 9.0
 instance Traversable Hierarchy where
-  traverse f n = Node (nodeEntityId n) <$> f (nodeEntity n) <*> traverse (traverse f) (nodeChildren n)
+  traverse f n =
+    Node (nodeEntityId n) <$> f (nodeEntity n) <*> traverse (traverse f) (nodeChildren n)
 
+-- | Convert a hierarchy to a list of entity IDs and components.
+--
+-- @since 9.0
 toList :: Hierarchy a -> [(EntityID, a)]
 toList n = (nodeEntityId n, nodeEntity n) : concatMap toList (nodeChildren n)
 
+-- | Fold a hierarchy with a function that takes the entity ID, entity, and accumulator.
+--
+-- @since 9.0
 foldWithKey :: (EntityID -> a -> b -> b) -> Hierarchy a -> b -> b
 foldWithKey f n b = f (nodeEntityId n) (nodeEntity n) (foldr (foldWithKey f) b (nodeChildren n))
 
+-- | Map a hierarchy with a function that takes the entity ID and entity.
+--
+-- @since 9.0
 mapWithKey :: (EntityID -> a -> b) -> Hierarchy a -> Hierarchy b
-mapWithKey f n = Node (nodeEntityId n) (f (nodeEntityId n) (nodeEntity n)) (map (mapWithKey f) (nodeChildren n))
+mapWithKey f n =
+  Node (nodeEntityId n) (f (nodeEntityId n) (nodeEntity n)) (map (mapWithKey f) (nodeChildren n))
 
+-- | Map a hierarchy with a function that takes the entity ID, entity, and accumulator.
+--
+-- @since 9.0
 mapWithAccum :: (EntityID -> a -> b -> (c, b)) -> b -> Hierarchy a -> Hierarchy c
 mapWithAccum f b n = case f (nodeEntityId n) (nodeEntity n) b of
   (c, b') -> Node (nodeEntityId n) c (map (mapWithAccum f b') (nodeChildren n))
 
+-- | System to read a hierarchy of parents to children with the given query.
+--
+-- @since 9.0
 hierarchy ::
-  (ArrowQueryReader q, ArrowDynamicQueryReader q, ArrowReaderSystem q arr) =>
+  (ArrowQueryReader q, ArrowDynamicQueryReader q, MonadReaderSystem q s) =>
   EntityID ->
+  i ->
   q i a ->
-  arr i (Maybe (Hierarchy a))
-hierarchy e q = proc i -> do
+  s (Maybe (Hierarchy a))
+hierarchy e i q = do
   children <-
     S.all
-      ( proc i -> do
+      ()
+      ( proc () -> do
           entity <- Q.entity -< ()
           Children cs <- Q.fetch -< ()
           a <- q -< i
           returnA -< (entity, (cs, a))
       )
-      -<
-        i
   let childMap = Map.fromList children
-  returnA -< hierarchy' e childMap
+  return $ hierarchy' e childMap
 
--- | Build all hierarchies of parents to children with the given query.
+-- | Build all hierarchies of parents to children, joined with the given query.
+--
+-- @since 9.0
 hierarchies ::
-  (ArrowQueryReader q, ArrowDynamicQueryReader q, ArrowReaderSystem q arr) =>
+  (ArrowQueryReader q, ArrowDynamicQueryReader q, MonadReaderSystem q s) =>
+  i ->
   q i a ->
-  arr i [Hierarchy a]
-hierarchies q = proc i -> do
+  s [Hierarchy a]
+hierarchies i q = do
   children <-
     S.all
-      ( proc i -> do
+      ()
+      ( proc () -> do
           entity <- Q.entity -< ()
           Children cs <- Q.fetch -< ()
           a <- q -< i
           returnA -< (entity, (cs, a))
       )
-      -<
-        i
+
   let childMap = Map.fromList children
-  roots <- S.filter Q.entity $ with @Children <> without @Parent -< ()
-  returnA -< mapMaybe (`hierarchy'` childMap) roots
+  roots <- S.filter () Q.entity $ with @Children <> without @Parent
+  return $ mapMaybe (`hierarchy'` childMap) roots
 
+-- | Build a hierarchy of parents to children.
+--
+-- @since 9.0
 hierarchy' :: EntityID -> Map EntityID (Set EntityID, a) -> Maybe (Hierarchy a)
 hierarchy' e childMap = case Map.lookup e childMap of
   Just (cs, a) ->
diff --git a/src/Aztecs/Input.hs b/src/Aztecs/Input.hs
--- a/src/Aztecs/Input.hs
+++ b/src/Aztecs/Input.hs
@@ -1,6 +1,14 @@
 {-# LANGUAGE DeriveAnyClass #-}
 {-# LANGUAGE DeriveGeneric #-}
 
+-- |
+-- Module      : Aztecs.Input
+-- Copyright   : (c) Matt Hunzinger, 2025
+-- License     : BSD-style (see the LICENSE file in the distribution)
+--
+-- Maintainer  : matt@hunzinger.me
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
 module Aztecs.Input
   ( Key (..),
     InputMotion (..),
@@ -27,6 +35,9 @@
 import Linear (V2 (..))
 import Linear.Affine (Point (..))
 
+-- | Keyboard key.
+--
+-- @since 9.0
 data Key
   = KeyA
   | KeyB
@@ -126,42 +137,66 @@
   deriving (Show, Eq, Ord, Enum, Bounded, Generic, NFData)
 
 -- | Keyboard input component.
+--
+-- @since 9.0
 data KeyboardInput = KeyboardInput
   { -- | Keyboard events that occured this frame.
+    --
+    -- @since 9.0
     keyboardEvents :: !(Map Key InputMotion),
     -- | Keys that are currently pressed.
+    --
+    -- @since 9.0
     keyboardPressed :: !(Set Key)
   }
   deriving (Show, Generic, NFData)
 
+-- | @since 9.0
 instance Component KeyboardInput
 
+-- | Empty keyboard input.
+--
+-- @since 9.0
 keyboardInput :: KeyboardInput
 keyboardInput = KeyboardInput Map.empty Set.empty
 
+-- | Input motion kind.
+--
+-- @since 9.0
 data InputMotion = Pressed | Released
   deriving (Show, Eq, Generic, NFData)
 
 -- | @True@ if this key is currently pressed.
+--
+-- @since 9.0
 isKeyPressed :: Key -> KeyboardInput -> Bool
 isKeyPressed key kb = Set.member key $ keyboardPressed kb
 
 -- | Check for a key event that occured this frame.
+--
+-- @since 9.0
 keyEvent :: Key -> KeyboardInput -> Maybe InputMotion
 keyEvent key kb = Map.lookup key $ keyboardEvents kb
 
 -- | @True@ if this key was pressed this frame.
+--
+-- @since 9.0
 wasKeyPressed :: Key -> KeyboardInput -> Bool
 wasKeyPressed key kb = case keyEvent key kb of
   Just Pressed -> True
   _ -> False
 
 -- | @True@ if this key was released this frame.
+--
+-- @since 9.0
 wasKeyReleased :: Key -> KeyboardInput -> Bool
 wasKeyReleased key kb = case keyEvent key kb of
   Just Released -> True
   _ -> False
 
+-- | Handle a keyboard event.
+--
+-- @since 9.0
 handleKeyboardEvent :: Key -> InputMotion -> KeyboardInput -> KeyboardInput
 handleKeyboardEvent key motion kb =
   KeyboardInput
@@ -171,6 +206,9 @@
         Released -> Set.delete key $ keyboardPressed kb
     }
 
+-- | Mouse button kind.
+--
+-- @since 9.0
 data MouseButton
   = ButtonLeft
   | ButtonMiddle
@@ -182,21 +220,36 @@
   deriving (Eq, Ord, Show, Generic, NFData)
 
 -- | Mouse input component.
+--
+-- @since 9.0
 data MouseInput = MouseInput
   { -- | Mouse position in screen-space.
+    --
+    -- @since 9.0
     mousePosition :: !(Point V2 Int),
     -- | Mouse offset since last frame.
+    --
+    -- @since 9.0
     mouseOffset :: !(V2 Int),
     -- | Mouse button states.
+    --
+    -- @since 9.0
     mouseButtons :: !(Map MouseButton InputMotion)
   }
   deriving (Show, Generic, NFData)
 
+-- | @since 9.0
 instance Component MouseInput
 
+-- | Empty mouse input.
+--
+-- @since 9.0
 mouseInput :: MouseInput
 mouseInput = MouseInput (P 0) (V2 0 0) Map.empty
 
+-- | Handle a mouse motion event.
+--
+-- @since 9.0
 handleMouseMotion :: V2 Int -> MouseInput -> MouseInput
 handleMouseMotion delta mouse =
   mouse
diff --git a/src/Aztecs/Time.hs b/src/Aztecs/Time.hs
--- a/src/Aztecs/Time.hs
+++ b/src/Aztecs/Time.hs
@@ -1,15 +1,30 @@
 {-# LANGUAGE DeriveGeneric #-}
 {-# LANGUAGE GeneralizedNewtypeDeriving #-}
 
+-- |
+-- Module      : Aztecs.Time
+-- Copyright   : (c) Matt Hunzinger, 2025
+-- License     : BSD-style (see the LICENSE file in the distribution)
+--
+-- Maintainer  : matt@hunzinger.me
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
 module Aztecs.Time (Time (..)) where
 
 import Aztecs.ECS
 import Control.DeepSeq
-import Data.Word (Word32)
+import Data.Word
 import GHC.Generics
 
 -- | Time component.
-newtype Time = Time {elapsedMS :: Word32}
+--
+-- @since 9.0
+newtype Time = Time
+  { -- | Elapsed time (in milliseconds).
+    --
+    -- @since 9.0
+    elapsedMS :: Word32
+  }
   deriving (Eq, Ord, Num, Show, Generic, NFData)
 
 instance Component Time
diff --git a/src/Aztecs/Transform.hs b/src/Aztecs/Transform.hs
--- a/src/Aztecs/Transform.hs
+++ b/src/Aztecs/Transform.hs
@@ -7,6 +7,14 @@
 {-# LANGUAGE ScopedTypeVariables #-}
 {-# LANGUAGE TypeApplications #-}
 
+-- |
+-- Module      : Aztecs.Transform
+-- Copyright   : (c) Matt Hunzinger, 2025
+-- License     : BSD-style (see the LICENSE file in the distribution)
+--
+-- Maintainer  : matt@hunzinger.me
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
 module Aztecs.Transform
   ( -- * Transform
     Transform (..),
@@ -33,71 +41,125 @@
 import Aztecs.ECS
 import qualified Aztecs.ECS.Access as A
 import qualified Aztecs.ECS.Query as Q
-import qualified Aztecs.ECS.System as S
 import Aztecs.Hierarchy (Hierarchy, hierarchies, mapWithAccum, toList)
-import Control.Arrow (Arrow (..), (>>>))
 import Control.DeepSeq
-import GHC.Generics (Generic)
-import Linear (V2 (..))
+import GHC.Generics
+import Linear
 
 -- | Transform component.
+--
+-- @since 9.0
 data Transform v r = Transform
-  { transformTranslation :: !v,
+  { -- | Translation.
+    --
+    -- @since 9.0
+    transformTranslation :: !v,
+    -- | Rotation.
+    --
+    -- @since 9.0
     transformRotation :: !r,
+    -- | Scale.
+    --
+    -- @since 9.0
     transformScale :: !v
   }
   deriving (Eq, Show, Generic, NFData)
 
+-- | @since 9.0
 instance (Num v, Num r) => Semigroup (Transform v r) where
   Transform t1 r1 s1 <> Transform t2 r2 s2 = Transform (t1 + t2) (r1 + r2) (s1 + s2)
 
+-- | @since 9.0
 instance (Num v, Num r) => Monoid (Transform v r) where
   mempty = Transform 0 0 0
 
 -- | 2D transform component.
+--
+-- @since 9.0
 type Transform2D = Transform (V2 Int) Int
 
 -- | Empty transform.
+--
+-- @since 9.0
 transform2d :: Transform2D
 transform2d = Transform (V2 0 0) 0 (V2 1 1)
 
+-- | @since 9.0
 instance Component (Transform (V2 Int) Int)
 
 -- | Size component.
+--
+-- @since 9.0
 newtype Size v = Size {unSize :: v}
   deriving (Generic, NFData)
 
+-- | @since 9.0
 type Size2D = Size (V2 Int)
 
+-- | Empty size.
+--
+-- @since 9.0
 size2D :: Size (V2 Integer)
 size2D = Size (V2 0 0)
 
+-- | @since 9.0
 instance Component (Size (V2 Int))
 
+-- | Propagate a hierarchy of transform components.
+--
+-- @since 9.0
 propagateHierarchy :: (Component a, Monoid a) => Hierarchy a -> Hierarchy a
 propagateHierarchy = mapWithAccum (\_ t acc -> let t' = t <> acc in (t', t')) mempty
 
 -- | Propagate and update all hierarchies of transform components.
+--
+-- @since 9.0
 update ::
-  forall q arr b m a.
+  forall q s b m a.
   ( ArrowQueryReader q,
     ArrowDynamicQueryReader q,
-    ArrowReaderSystem q arr,
-    ArrowQueueSystem b m arr,
+    MonadReaderSystem q s,
     Component a,
-    Monoid a
+    Monoid a,
+    MonadAccess b m
   ) =>
-  arr () ()
-update = propagate @_ @_ @a >>> S.queue (mapM_ $ mapM_ (uncurry A.insert) . toList)
+  s (m ())
+update = mapM_ (\(e, a) -> A.insert e $ bundle a) . concatMap toList <$> propagate @_ @_ @a
 
 -- | Propagate and update all hierarchies of transform components.
-update2d :: (ArrowQueryReader q, ArrowDynamicQueryReader q, ArrowReaderSystem q arr, ArrowQueueSystem b m arr) => arr () ()
-update2d = propagate @_ @_ @Transform2D >>> S.queue (mapM_ $ mapM_ (uncurry A.insert) . toList)
+--
+-- @since 9.0
+update2d ::
+  ( ArrowQueryReader q,
+    ArrowDynamicQueryReader q,
+    MonadReaderSystem q s,
+    MonadAccess b m
+  ) =>
+  s (m ())
+update2d = update @_ @_ @_ @_ @Transform2D
 
+-- | Propagate all hierarchies of transform components.
+--
+-- @since 9.0
 propagate ::
-  (ArrowQueryReader q, ArrowDynamicQueryReader q, ArrowReaderSystem q arr, Component a, Monoid a) =>
-  arr () [Hierarchy a]
-propagate = hierarchies Q.fetch >>> arr (map propagateHierarchy)
+  ( ArrowQueryReader q,
+    ArrowDynamicQueryReader q,
+    MonadReaderSystem q s,
+    Component a,
+    Monoid a
+  ) =>
+  s [Hierarchy a]
+propagate = do
+  hs <- hierarchies () Q.fetch
+  return $ map propagateHierarchy hs
 
-propagate2d :: (ArrowQueryReader q, ArrowDynamicQueryReader q, ArrowReaderSystem q arr) => arr () [Hierarchy Transform2D]
+-- | Propagate all hierarchies of `Transform2D` components.
+--
+-- @since 9.0
+propagate2d ::
+  ( ArrowQueryReader q,
+    ArrowDynamicQueryReader q,
+    MonadReaderSystem q s
+  ) =>
+  s [Hierarchy Transform2D]
 propagate2d = propagate
diff --git a/src/Aztecs/Window.hs b/src/Aztecs/Window.hs
--- a/src/Aztecs/Window.hs
+++ b/src/Aztecs/Window.hs
@@ -1,17 +1,30 @@
 {-# LANGUAGE DeriveAnyClass #-}
 {-# LANGUAGE DeriveGeneric #-}
 
+-- |
+-- Module      : Aztecs.Window
+-- Copyright   : (c) Matt Hunzinger, 2025
+-- License     : BSD-style (see the LICENSE file in the distribution)
+--
+-- Maintainer  : matt@hunzinger.me
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
 module Aztecs.Window (Window (..)) where
 
 import Aztecs.ECS
-import Control.DeepSeq (NFData)
-import GHC.Generics (Generic)
+import Control.DeepSeq
+import GHC.Generics
 
 -- | Window component.
-data Window = Window
+--
+-- @since 9.0
+newtype Window = Window
   { -- | Window title.
-    windowTitle :: !String
+    --
+    -- @since 9.0
+    windowTitle :: String
   }
   deriving (Show, Generic, NFData)
 
+-- | @since 9.0
 instance Component Window
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -13,13 +13,11 @@
 import Aztecs
 import Aztecs.ECS.Component (ComponentID (ComponentID))
 import qualified Aztecs.ECS.Query as Q
-import qualified Aztecs.ECS.System as S
 import qualified Aztecs.ECS.World as W
-import Aztecs.Hierarchy (Children (..), Parent (..))
-import qualified Aztecs.Hierarchy as Hierarchy
-import Control.Arrow (Arrow (..), returnA, (&&&))
+import Control.Arrow
 import Control.DeepSeq
-import qualified Data.Set as Set
+import Data.Functor.Identity (Identity (runIdentity))
+import Data.Word
 import GHC.Generics
 import Test.Hspec
 import Test.QuickCheck
@@ -38,18 +36,30 @@
 
 main :: IO ()
 main = hspec $ do
+  describe "Aztecs.ECS.Query.single" $ do
+    it "queries a single entity" prop_querySingle
+  describe "Aztecs.ECS.Query.mapSingle" $ do
+    it "maps a single entity" $ property prop_queryMapSingle
   describe "Aztecs.ECS.Query.all" $ do
+    it "queries an empty world" prop_queryEmpty
     it "queries dynamic components" $ property prop_queryDyn
     it "queries a typed component" $ property prop_queryTypedComponent
     it "queries 2 typed components" $ property prop_queryTwoTypedComponents
     it "queries 3 typed components" $ property prop_queryThreeTypedComponents
-  describe "Aztecs.ECS.Hierarchy.update" $ do
-    it "adds Parent components to children" $ property prop_addParents
-    it "removes Parent components from removed children" $ property prop_removeParents
-  describe "Aztecs.ECS.Schedule" $ do
-    it "queries entities" $ property prop_scheduleQueryEntity
-    it "updates components" prop_scheduleUpdate
 
+{-TODO
+describe "Aztecs.ECS.System.mapSingle" $ do
+  it "maps a single entity" $ property prop_systemMapSingle
+describe "Aztecs.ECS.Hierarchy.update" $ do
+  it "adds Parent components to children" $ property prop_addParents
+  it "removes Parent components from removed children" $ property prop_removeParents
+-}
+
+prop_queryEmpty :: Expectation
+prop_queryEmpty = do
+  res <- fst $ Q.all () (Q.fetch @_ @X) $ W.entities W.empty
+  res `shouldMatchList` []
+
 -- | Query all components from a list of `ComponentID`s.
 queryComponentIds ::
   forall q a.
@@ -75,35 +85,60 @@
             (e, wAcc') = W.spawn b wAcc
          in ((e, cs) : acc, wAcc')
       (es, w) = foldr spawn ([], W.empty) xs
-      go (e, cs) =
+      go (e, cs) = do
         let q = queryComponentIds $ map snd cs
-            (res, _) = Q.all () q $ W.entities w
-         in res `shouldContain` [(e, map fst cs)]
+        res <- fst $ Q.all () q $ W.entities w
+        return $ res `shouldContain` [(e, map fst cs)]
    in mapM_ go es
 
 prop_queryTypedComponent :: [X] -> Expectation
-prop_queryTypedComponent xs =
+prop_queryTypedComponent xs = do
   let w = foldr (\x -> snd . W.spawn (bundle x)) W.empty xs
-      (res, _) = Q.all () Q.fetch $ W.entities w
-   in res `shouldMatchList` xs
+  res <- fst $ Q.all () Q.fetch $ W.entities w
+  res `shouldMatchList` xs
 
 prop_queryTwoTypedComponents :: [(X, Y)] -> Expectation
-prop_queryTwoTypedComponents xys =
+prop_queryTwoTypedComponents xys = do
   let w = foldr (\(x, y) -> snd . W.spawn (bundle x <> bundle y)) W.empty xys
-      (res, _) = Q.all () (Q.fetch &&& Q.fetch) $ W.entities w
-   in res `shouldMatchList` xys
+  res <- fst $ Q.all () (Q.fetch &&& Q.fetch) $ W.entities w
+  res `shouldMatchList` xys
 
 prop_queryThreeTypedComponents :: [(X, Y, Z)] -> Expectation
-prop_queryThreeTypedComponents xyzs =
+prop_queryThreeTypedComponents xyzs = do
   let w = foldr (\(x, y, z) -> snd . W.spawn (bundle x <> bundle y <> bundle z)) W.empty xyzs
       q = do
         x <- Q.fetch
         y <- Q.fetch
         z <- Q.fetch
         pure (x, y, z)
-      (res, _) = Q.all () q $ W.entities w
-   in res `shouldMatchList` xyzs
+  res <- fst $ Q.all () q $ W.entities w
+  res `shouldMatchList` xyzs
 
+prop_querySingle :: Expectation
+prop_querySingle =
+  let (_, w) = W.spawn (bundle $ X 1) W.empty
+      (res, _) = Q.single () Q.fetch $ W.entities w
+   in res `shouldBe` X 1
+
+prop_queryMapSingle :: Word8 -> Expectation
+prop_queryMapSingle n =
+  let (_, w) = W.spawn (bundle $ X 0) W.empty
+      q = Q.fetch >>> arr (\(X x) -> X $ x + 1) >>> Q.set
+      w' = foldr (\_ es -> snd . runIdentity $ Q.mapSingle () q es) (W.entities w) [1 .. n]
+      (res, _) = Q.single () Q.fetch w'
+   in res `shouldBe` X (fromIntegral n)
+
+{-TODO
+prop_systemMapSingle :: Word8 -> Expectation
+prop_systemMapSingle n =
+  let (_, w) = W.spawn (bundle $ X 0) W.empty
+      q = Q.adjust (\_ (X x) -> X $ x + 1)
+      s =  S.mapSingle q
+      go _ wAcc = let (_, _, wAcc') = runIdentity $ runSchedule s wAcc () in wAcc'
+      w' = foldr go w [1 .. n]
+      (res, _) = Q.single () Q.fetch (W.entities w')
+   in res `shouldBe` X (fromIntegral n)
+
 prop_addParents :: Expectation
 prop_addParents = do
   let (_, w) = W.spawnEmpty W.empty
@@ -121,7 +156,9 @@
   (_, _, w'''') <- runSchedule (system Hierarchy.update) w''' ()
   let (res, _) = Q.all () (Q.fetch @_ @Parent) $ W.entities w''''
   res `shouldMatchList` []
+-}
 
+{-
 prop_scheduleQueryEntity :: [X] -> Expectation
 prop_scheduleQueryEntity xs = do
   let go x (eAcc, wAcc) = let (e, wAcc') = W.spawn (bundle x) wAcc in (e : eAcc, wAcc')
@@ -151,3 +188,4 @@
             ()
       let shouldQuit = (lastShouldQuit || x > 1)
   returnA -< shouldQuit
+-}
