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.10.0
+version:       0.11.0
 license:       BSD-3-Clause
 license-file:  LICENSE
 maintainer:    matt@hunzinger.me
@@ -25,48 +25,23 @@
 
 library
     exposed-modules:
-        Aztecs
         Aztecs.ECS
         Aztecs.ECS.Access
-        Aztecs.ECS.Access.Class
         Aztecs.ECS.Component
         Aztecs.ECS.Entity
         Aztecs.ECS.Query
-        Aztecs.ECS.Query.Class
         Aztecs.ECS.Query.Dynamic
-        Aztecs.ECS.Query.Dynamic.Class
-        Aztecs.ECS.Query.Dynamic.Reader
-        Aztecs.ECS.Query.Dynamic.Reader.Class
-        Aztecs.ECS.Query.Reader
-        Aztecs.ECS.Query.Reader.Class
         Aztecs.ECS.System
-        Aztecs.ECS.System.Class
-        Aztecs.ECS.System.Dynamic.Class
-        Aztecs.ECS.System.Dynamic.Reader.Class
-        Aztecs.ECS.System.Reader.Class
         Aztecs.ECS.View
         Aztecs.ECS.World
         Aztecs.ECS.World.Archetype
         Aztecs.ECS.World.Archetypes
         Aztecs.ECS.World.Bundle
-        Aztecs.ECS.World.Bundle.Class
         Aztecs.ECS.World.Bundle.Dynamic
-        Aztecs.ECS.World.Bundle.Dynamic.Class
         Aztecs.ECS.World.Components
         Aztecs.ECS.World.Entities
         Aztecs.ECS.World.Storage
         Aztecs.ECS.World.Storage.Dynamic
-        Aztecs.Asset
-        Aztecs.Asset.AssetLoader
-        Aztecs.Asset.AssetLoader.Class
-        Aztecs.Asset.AssetServer
-        Aztecs.Asset.Class
-        Aztecs.Camera
-        Aztecs.Hierarchy
-        Aztecs.Input
-        Aztecs.Time
-        Aztecs.Transform
-        Aztecs.Window
 
     hs-source-dirs:   src
     default-language: Haskell2010
@@ -75,10 +50,17 @@
         base >=4.2 && <5,
         containers >=0.6,
         deepseq >=1,
-        linear >=1,
         mtl >=2,
-        parallel >=3,
         stm >=2
+
+executable ecs
+    main-is:          examples/ECS.hs
+    default-language: Haskell2010
+    ghc-options:      -Wall
+    build-depends:
+        base,
+        aztecs,
+        deepseq >=1
 
 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
@@ -20,14 +20,14 @@
 
 instance Component Velocity
 
-query :: Query Position
-query = Q.fetch & Q.adjust (\(Velocity v) (Position p) -> Position $ p + v)
+q :: Query Position
+q = fetch & zipFetchMap (\(Velocity v) (Position p) -> Position $ p + v)
 
-run :: Query Position -> World -> [Position]
-run q = fst . runIdentity . Q.map q . entities
+run :: World -> [Position]
+run = fst . runIdentity . Q.query 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 query) w]
+  defaultMain [bench "iter" $ nf run w]
diff --git a/examples/ECS.hs b/examples/ECS.hs
new file mode 100644
--- /dev/null
+++ b/examples/ECS.hs
@@ -0,0 +1,35 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+
+module Main where
+
+import Aztecs.ECS
+import Control.DeepSeq
+import Control.Monad
+import Control.Monad.IO.Class
+import Data.Function
+import GHC.Generics
+
+newtype Position = Position Int deriving (Show, Generic, NFData)
+
+instance Component Position
+
+newtype Velocity = Velocity Int deriving (Show, Generic, NFData)
+
+instance Component Velocity
+
+move :: (Monad m) => QueryT m Position
+move = fetch & zipFetchMap (\(Velocity v) (Position p) -> Position $ p + v)
+
+run :: SystemT IO ()
+run = do
+  positions <- query move
+  liftIO $ print positions
+
+app :: AccessT IO ()
+app = do
+  _ <- spawn $ bundle (Position 0) <> bundle (Velocity 1)
+  forever $ system run
+
+main :: IO ()
+main = runAccessT_ app
diff --git a/src/Aztecs.hs b/src/Aztecs.hs
deleted file mode 100644
--- a/src/Aztecs.hs
+++ /dev/null
@@ -1,88 +0,0 @@
--- | Aztecs is a type-safe and friendly ECS for games and more.
---
--- An ECS is a modern approach to organizing your application state as a database,
--- providing patterns for data-oriented design and parallel processing.
---
--- The ECS architecture is composed of three main concepts:
---
--- === Entities
--- An entity is an object comprised of zero or more components.
--- In Aztecs, entities are represented by their `EntityID`, a unique identifier.
---
--- === Components
--- A `Component` holds the data for a particular aspect of an entity.
--- For example, a zombie entity might have a @Health@ and a @Transform@ component.
---
--- > newtype Position = Position Int deriving (Show)
--- > instance Component Position
--- >
--- > newtype Velocity = Velocity Int deriving (Show)
--- > instance Component Velocity
---
--- === Systems
--- A `System` is a pipeline that processes entities and their components.
--- Systems in Aztecs either run in sequence or in parallel automatically based on the components they access.
---
--- Systems can access game state in two ways:
---
--- ==== Access
--- An `Access` can be queued for full access to the `World`, after a system is complete.
--- `Access` allows for spawning, inserting, and removing components.
---
--- > setup :: System  () ()
--- > setup = S.queue . const . A.spawn_ $ bundle (Position 0) <> bundle (Velocity 1)
---
--- ==== Queries
--- A `Query` can read and write matching components.
---
--- > move :: System  () ()
--- > move =
--- >  S.map
--- >    ( proc () -> do
--- >        Velocity v <- Q.fetch -< ()
--- >        Position p <- Q.fetch -< ()
--- >        Q.set -< Position $ p + v
--- >    )
--- >    >>> S.run print
---
--- Finally, systems can be run on a `World` to produce a result.
---
--- > main :: IO ()
--- > main = runSystem_ $ setup >>> S.forever move
-module Aztecs
-  ( module Aztecs.ECS,
-    asset,
-    load,
-    Camera (..),
-    CameraTarget (..),
-    Key (..),
-    KeyboardInput (..),
-    isKeyPressed,
-    wasKeyPressed,
-    wasKeyReleased,
-    MouseInput (..),
-    Time (..),
-    Transform (..),
-    Transform2D,
-    transform2d,
-    Size (..),
-    Size2D,
-    size2D,
-    Window (..),
-  )
-where
-
-import Aztecs.Asset (asset, load)
-import Aztecs.Camera
-import Aztecs.ECS
-import Aztecs.Input
-  ( Key (..),
-    KeyboardInput (..),
-    MouseInput (..),
-    isKeyPressed,
-    wasKeyPressed,
-    wasKeyReleased,
-  )
-import Aztecs.Time
-import Aztecs.Transform
-import Aztecs.Window
diff --git a/src/Aztecs/Asset.hs b/src/Aztecs/Asset.hs
deleted file mode 100644
--- a/src/Aztecs/Asset.hs
+++ /dev/null
@@ -1,32 +0,0 @@
-{-# LANGUAGE AllowAmbiguousTypes #-}
-{-# LANGUAGE DerivingStrategies #-}
-{-# LANGUAGE FlexibleContexts #-}
-{-# LANGUAGE FlexibleInstances #-}
-{-# LANGUAGE RankNTypes #-}
-{-# 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 (..),
-    MonadAssetLoader (..),
-    Asset (..),
-    Handle (..),
-    lookupAsset,
-    setup,
-    loadAssets,
-    load,
-  )
-where
-
-import Aztecs.Asset.AssetLoader
-import Aztecs.Asset.AssetServer
-import Aztecs.Asset.Class
diff --git a/src/Aztecs/Asset/AssetLoader.hs b/src/Aztecs/Asset/AssetLoader.hs
deleted file mode 100644
--- a/src/Aztecs/Asset/AssetLoader.hs
+++ /dev/null
@@ -1,99 +0,0 @@
-{-# LANGUAGE AllowAmbiguousTypes #-}
-{-# LANGUAGE ApplicativeDo #-}
-{-# LANGUAGE DerivingStrategies #-}
-{-# LANGUAGE FlexibleContexts #-}
-{-# LANGUAGE FlexibleInstances #-}
-{-# LANGUAGE GeneralizedNewtypeDeriving #-}
-{-# LANGUAGE MultiParamTypeClasses #-}
-
--- |
--- 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
-  ( AssetLoader,
-    AssetLoaderT (..),
-    MonadAssetLoader (..),
-    load,
-    loadQuery,
-  )
-where
-
-import Aztecs.Asset.AssetLoader.Class
-import Aztecs.Asset.AssetServer
-import Aztecs.Asset.Class
-import Aztecs.ECS
-import Aztecs.ECS.Query (QueryT (..))
-import qualified Aztecs.ECS.Query as Q
-import Aztecs.ECS.Query.Dynamic (DynamicQueryT (DynamicQuery, runDynQuery))
-import qualified Aztecs.ECS.System as S
-import Control.Concurrent
-import Control.Monad.Identity
-import Control.Monad.State.Strict
-import Control.Monad.Writer
-import Data.IORef
-import qualified Data.Map.Strict as Map
-
--- | @since 0.9
-type AssetLoader a o = AssetLoaderT a Identity o
-
--- | Asset loader monad.
---
--- @since 0.9
-newtype AssetLoaderT a m o = AssetLoaderT
-  { -- | State of the asset loader.
-    --
-    -- @since 0.9
-    unAssetLoader :: StateT (AssetServer a) m o
-  }
-  deriving newtype (Functor, Applicative, Monad)
-
--- | @since 0.9
-instance (Monad m, Asset a) => MonadAssetLoader a (AssetLoaderT a m) where
-  asset path cfg = AssetLoaderT $ do
-    server <- get
-    let assetId = nextAssetId server
-        go = do
-          v <- newIORef Nothing
-          _ <- forkIO $ do
-            a <- loadAsset path cfg
-            writeIORef v (Just a)
-          return v
-    put $
-      server
-        { loadingAssets = Map.insert assetId (Left go) (loadingAssets server),
-          nextAssetId = AssetId (unAssetId assetId + 1)
-        }
-    return $ Handle assetId
-
--- | Query to load assets.
---
--- @since 0.9
-loadQuery :: (Asset a) => AssetLoader a o -> Query o
-loadQuery a =
-  -- TODO
-  Query $ \cs ->
-    let q =
-          Q.adjustM
-            ( \_ server -> do
-                let (o, server') = runState (unAssetLoader a) server
-                tell [o]
-                return server'
-            )
-            (pure ())
-        (rws, cs', dynQ) = runQuery q cs
-     in ( rws,
-          cs',
-          DynamicQuery $ \arch ->
-            let ((_, arch'), os) = runWriter $ runDynQuery dynQ arch in return (os, arch')
-        )
-
--- | System to load assets.
---
--- @since 0.9
-load :: (MonadSystem Query s, Asset a) => AssetLoader a o -> s o
-load a = S.mapSingle $ loadQuery a
diff --git a/src/Aztecs/Asset/AssetLoader/Class.hs b/src/Aztecs/Asset/AssetLoader/Class.hs
deleted file mode 100644
--- a/src/Aztecs/Asset/AssetLoader/Class.hs
+++ /dev/null
@@ -1,23 +0,0 @@
-{-# LANGUAGE FunctionalDependencies #-}
-
--- |
--- 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
-import Aztecs.Asset.Class
-
--- | Monadic interface for loading assets.
---
--- @since 0.9
-class MonadAssetLoader a m | m -> a where
-  -- | Load an asset from a file path with a configuration.
-  --
-  -- @since 0.9
-  asset :: FilePath -> AssetConfig a -> m (Handle a)
diff --git a/src/Aztecs/Asset/AssetServer.hs b/src/Aztecs/Asset/AssetServer.hs
deleted file mode 100644
--- a/src/Aztecs/Asset/AssetServer.hs
+++ /dev/null
@@ -1,142 +0,0 @@
-{-# LANGUAGE AllowAmbiguousTypes #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE DerivingStrategies #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-{-# 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)
-module Aztecs.Asset.AssetServer
-  ( AssetId (..),
-    Handle (..),
-    AssetServer (..),
-    assetServer,
-    lookupAsset,
-    setup,
-    loadAssets,
-    loadAssetServer,
-  )
-where
-
-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 Control.DeepSeq
-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
-
--- | Unique identifier for an asset.
---
--- @since 0.9
-newtype AssetId = AssetId
-  { -- | Unique integer identifier.
-    --
-    -- @since 0.9
-    unAssetId :: Int
-  }
-  deriving (Eq, Ord, Show)
-
--- | Asset server.
---
--- @since 0.9
-data AssetServer a = AssetServer
-  { -- | Loaded assets.
-    --
-    -- @since 0.9
-    assetServerAssets :: !(Map AssetId a),
-    -- | Assets currently being loaded.
-    --
-    -- @since 0.9
-    loadingAssets :: !(Map AssetId (Either (IO (IORef (Maybe a))) (IORef (Maybe a)))),
-    -- | Next unique asset identifier.
-    --
-    -- @since 0.9
-    nextAssetId :: !AssetId
-  }
-  deriving (Generic)
-
--- | @since 0.9
-instance (Typeable a) => Component (AssetServer a)
-
--- | @since 0.9
-instance NFData (AssetServer a) where
-  rnf = rwhnf
-
--- | Empty asset server.
---
--- @since 0.9
-assetServer :: AssetServer a
-assetServer =
-  AssetServer
-    { assetServerAssets = Map.empty,
-      loadingAssets = Map.empty,
-      nextAssetId = AssetId 0
-    }
-
--- | Handle to an asset.
---
--- @since 0.9
-newtype Handle a = Handle
-  { -- | Asset ID.
-    --
-    -- @since 0.9
-    handleId :: AssetId
-  }
-  deriving (Eq, Ord, Show)
-
--- | @since 0.9
-instance NFData (Handle a) where
-  rnf = rwhnf
-
--- | Lookup an asset by its handle.
---
--- @since 0.9
-lookupAsset :: Handle a -> AssetServer a -> Maybe a
-lookupAsset h server = Map.lookup (handleId h) (assetServerAssets server)
-
--- | Setup the asset server.
---
--- @since 0.9
-setup :: forall m b a. (Typeable a, MonadAccess b m) => m ()
-setup = A.spawn_ . bundle $ assetServer @a
-
--- | Load any pending assets.
---
--- @since 0.9
-loadAssets :: forall a q s m. (Typeable a, QueryF m q, Applicative q, MonadSystem q s, MonadIO m) => s ()
-loadAssets = void $ S.map (Q.adjustM (\_ s -> loadAssetServer @m @a s) (pure ())) -- TODO Q.setM
-
--- | Load any pending assets in an `AssetServer`.
---
--- @since 0.9
-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
deleted file mode 100644
--- a/src/Aztecs/Asset/Class.hs
+++ /dev/null
@@ -1,27 +0,0 @@
-{-# 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
-
--- | Loadable asset.
---
--- @since 0.9
-class (Typeable a) => Asset a where
-  -- | Configuration for loading an asset.
-  --
-  -- @since 0.9
-  type AssetConfig a
-
-  -- | Load an asset from a file path with a configuration.
-  --
-  -- @since 0.9
-  loadAsset :: FilePath -> AssetConfig a -> IO a
diff --git a/src/Aztecs/Camera.hs b/src/Aztecs/Camera.hs
deleted file mode 100644
--- a/src/Aztecs/Camera.hs
+++ /dev/null
@@ -1,78 +0,0 @@
-{-# 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 (..),
-    addCameraTargets,
-  )
-where
-
-import Aztecs.ECS
-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
-import Control.DeepSeq
-import GHC.Generics
-import Linear
-
--- | Camera component.
---
--- @since 0.9
-data Camera = Camera
-  { -- | Camera viewport size.
-    --
-    -- @since 0.9
-    cameraViewport :: !(V2 Int),
-    -- | Camera scale factor.
-    --
-    -- @since 0.9
-    cameraScale :: !(V2 Float)
-  }
-  deriving (Show, Generic, NFData)
-
--- | @since 0.9
-instance Component Camera
-
--- | Camera target component.
---
--- @since 0.9
-newtype CameraTarget = CameraTarget
-  { -- | This camera's target window.
-    --
-    -- @since 0.9
-    cameraTargetWindow :: EntityID
-  }
-  deriving (Eq, Show, Generic, NFData)
-
--- | @since 0.9
-instance Component CameraTarget
-
--- | Add `CameraTarget` components to entities with a new `Draw` component.
---
--- @since 0.9
-addCameraTargets ::
-  ( Applicative qr,
-    QueryReaderF qr,
-    DynamicQueryReaderF qr,
-    MonadReaderSystem qr s,
-    MonadAccess b m
-  ) =>
-  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 ()
-  return go
diff --git a/src/Aztecs/ECS.hs b/src/Aztecs/ECS.hs
--- a/src/Aztecs/ECS.hs
+++ b/src/Aztecs/ECS.hs
@@ -21,80 +21,32 @@
 --
 -- === Systems
 -- A `System` is a pipeline that processes entities and their components.
--- Systems in Aztecs either run in sequence or in parallel automatically based on the components they access.
---
--- Systems can access game state in two ways:
---
--- ==== Access
--- An `Access` can be queued for full access to the `World`, after a system is complete.
--- `Access` allows for spawning, inserting, and removing components.
---
--- > setup :: System  () ()
--- > setup = S.queue . const . A.spawn_ $ bundle (Position 0) <> bundle (Velocity 1)
---
--- ==== Queries
--- A `Query` can read and write matching components.
---
--- > move :: System  () ()
--- > move =
--- >  S.map
--- >    ( proc () -> do
--- >        Velocity v <- Q.fetch -< ()
--- >        Position p <- Q.fetch -< ()
--- >        Q.set -< Position $ p + v
--- >    )
--- >    >>> S.run print
---
--- Finally, systems can be run on a `World` to produce a result.
---
--- > main :: IO ()
--- > main = runSystem_ $ setup >>> S.forever move
 module Aztecs.ECS
-  ( Access,
+  ( module Aztecs.ECS.System,
+    module Aztecs.ECS.Query,
+    Access,
     AccessT,
-    MonadAccess,
     runAccessT,
     runAccessT_,
     Bundle,
-    MonoidBundle (..),
+    bundle,
+    fromDynBundle,
     DynamicBundle,
-    MonoidDynamicBundle (..),
+    dynBundle,
     Component (..),
     EntityID,
-    Query,
-    QueryT,
-    QueryReader,
-    QueryReaderF,
-    QueryF,
-    DynamicQueryReaderF,
-    DynamicQueryF,
-    QueryFilter,
-    with,
-    without,
-    System,
-    SystemT,
-    MonadReaderSystem,
-    MonadSystem,
+    spawn,
+    system,
+    concurrently,
     World,
   )
 where
 
 import Aztecs.ECS.Access
-import Aztecs.ECS.Component (Component (..))
-import Aztecs.ECS.Entity (EntityID)
-import Aztecs.ECS.Query
-  ( DynamicQueryF,
-    DynamicQueryReaderF,
-    Query,
-    QueryF,
-    QueryFilter,
-    QueryReaderF,
-    QueryT,
-    with,
-    without,
-  )
-import Aztecs.ECS.Query.Reader (QueryReader)
-import Aztecs.ECS.System
+import Aztecs.ECS.Component
+import Aztecs.ECS.Entity
+import Aztecs.ECS.Query hiding (query, querySingle, querySingleMaybe, readQueryEntities)
+import Aztecs.ECS.System hiding (concurrently)
 import Aztecs.ECS.World (World)
-import Aztecs.ECS.World.Bundle (Bundle, MonoidBundle (..))
-import Aztecs.ECS.World.Bundle.Dynamic (DynamicBundle, MonoidDynamicBundle (..))
+import Aztecs.ECS.World.Bundle
+import Aztecs.ECS.World.Bundle.Dynamic
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
@@ -1,4 +1,5 @@
 {-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE FlexibleInstances #-}
 {-# LANGUAGE GeneralizedNewtypeDeriving #-}
 {-# LANGUAGE MultiParamTypeClasses #-}
 {-# LANGUAGE TypeFamilies #-}
@@ -14,35 +15,32 @@
 module Aztecs.ECS.Access
   ( Access,
     AccessT (..),
-    MonadAccess (..),
+    spawn,
+    insert,
+    lookup,
+    remove,
+    despawn,
     runAccessT,
     runAccessT_,
     system,
+    concurrently,
   )
 where
 
-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 (DynamicQueryReader)
-import qualified Aztecs.ECS.Query.Dynamic.Reader as Q
-import Aztecs.ECS.Query.Reader
-import Aztecs.ECS.System
+import Aztecs.ECS.Component
+import Aztecs.ECS.Entity
+import Aztecs.ECS.System (SystemT (..), runSystemT)
+import qualified Aztecs.ECS.System as S
 import Aztecs.ECS.World (World (..))
 import qualified Aztecs.ECS.World as W
-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
+import Prelude hiding (lookup)
 
 -- | @since 0.9
 type Access = AccessT Identity
@@ -73,104 +71,80 @@
 runAccessT_ :: (Functor m) => AccessT m a -> m a
 runAccessT_ a = fmap fst . runAccessT a $ W.empty
 
--- | @since 0.9
-instance (Monad m) => MonadAccess Bundle (AccessT m) where
-  spawn b = AccessT $ do
-    !w <- get
-    let !(e, w') = W.spawn b w
-    put w'
-    return e
-  insert e c = AccessT $ do
-    !w <- get
-    let !w' = W.insert e c w
-    put w'
-  lookup e = AccessT $ do
-    !w <- get
-    return $ W.lookup e w
-  remove e = AccessT $ do
-    !w <- get
-    let !(a, w') = W.remove e w
-    put w'
-    return a
-  despawn e = AccessT $ do
-    !w <- get
-    let !(_, w') = W.despawn e w
-    put w'
+-- | Spawn an entity with a `Bundle`.
+--
+-- @since 0.11
+spawn :: (Monad m) => Bundle -> AccessT m EntityID
+spawn b = AccessT $ do
+  !w <- get
+  let !(e, w') = W.spawn b w
+  put w'
+  return e
 
--- | @since 0.9
-instance (Monad m) => MonadReaderSystem QueryReader (AccessT m) where
-  all 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 cIds dynQ
-  filter 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 cIds dynQ f'
+-- | Insert a `Bundle` into an entity.
+--
+-- @since 0.11
+insert :: (Monad m) => EntityID -> Bundle -> AccessT m ()
+insert e c = AccessT $ do
+  !w <- get
+  let !w' = W.insert e c w
+  put w'
 
--- | @since 0.9
-instance (Monad m) => MonadSystem (QueryT m) (AccessT m) where
-  map 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 (Q.reads rws <> Q.writes rws) dynQ
-  mapSingleMaybe 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 (Q.reads rws <> Q.writes rws) dynQ
-  filterMap 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 (Q.reads rws <> Q.writes rws) f' dynQ
+-- | Lookup a component by `EntityID`.
+--
+-- @since 0.11
+lookup :: (Monad m, Component a) => EntityID -> AccessT m (Maybe a)
+lookup e = AccessT $ do
+  !w <- get
+  return $ W.lookup e w
 
--- | @since 0.9
-instance (Monad m) => MonadDynamicReaderSystem DynamicQueryReader (AccessT m) where
-  allDyn cIds q = AccessT $ do
-    !w <- get
-    return . Q.allDyn cIds q $ entities w
-  filterDyn cIds q f = AccessT $ do
-    !w <- get
-    return . Q.filterDyn cIds f q $ entities w
+-- | Remove a component by `EntityID`.
+--
+-- @since 0.11
+remove :: (Monad m, Component a) => EntityID -> AccessT m (Maybe a)
+remove e = AccessT $ do
+  !w <- get
+  let !(a, w') = W.remove e w
+  put w'
+  return a
 
--- | @since 0.9
-instance (Monad m) => MonadDynamicSystem (DynamicQueryT m) (AccessT m) where
-  mapDyn cIds q = AccessT $ do
-    !w <- get
-    (as, es) <- lift . Q.mapDyn cIds q $ entities w
-    put w {entities = es}
-    return as
-  mapSingleMaybeDyn cIds q = AccessT $ do
-    !w <- get
-    (res, es) <- lift . Q.mapSingleMaybeDyn cIds q $ entities w
-    put w {entities = es}
-    return res
-  filterMapDyn cIds f q = AccessT $ do
-    !w <- get
-    (as, es) <- lift . Q.filterMapDyn cIds f q $ entities w
-    put w {entities = es}
-    return as
+-- | Despawn an entity by `EntityID`.
+--
+-- @since 0.11
+despawn :: (Monad m) => EntityID -> AccessT m ()
+despawn e = AccessT $ do
+  !w <- get
+  let !(_, w') = W.despawn e w
+  put w'
 
 -- | Run a `System`.
 --
--- @since 0.9
-system :: System a -> AccessT IO a
+-- @since 0.11
+system :: (Monad m) => SystemT m a -> AccessT m a
 system s = AccessT $ do
   !w <- get
+  let go f = do
+        es <- get
+        let es' = f es
+        put es'
+        return es'
+  (a, es) <- lift $ runStateT (runSystemT s go) (entities w)
+  put w {entities = es}
+  return a
+
+-- | Run a `System` concurrently.
+--
+-- @since 0.11
+concurrently :: SystemT IO a -> AccessT IO a
+concurrently s = AccessT $ do
+  !w <- get
   esVar <- lift . newTVarIO $ entities w
-  a <- lift . atomically $ runReaderT (runSystemT s) esVar
+  let go f = atomically $ do
+        es <- readTVar esVar
+        let es' = f es
+        writeTVar esVar es'
+        return es'
+  a <- liftIO $ S.concurrently s go
   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
deleted file mode 100644
--- a/src/Aztecs/ECS/Access/Class.hs
+++ /dev/null
@@ -1,52 +0,0 @@
-{-# 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
-import Aztecs.ECS.Entity
-import Aztecs.ECS.World.Bundle.Class
-
--- | Monadic access to a `World`.
---
--- @since 0.9
-class (MonoidBundle b, Monad m) => MonadAccess b m | m -> b where
-  -- | Spawn an entity with a component.
-  --
-  -- @since 0.9
-  spawn :: b -> m EntityID
-
-  -- | Spawn an entity with a component.
-  --
-  -- @since 0.9
-  spawn_ :: b -> m ()
-  spawn_ c = do
-    _ <- spawn c
-    return ()
-
-  -- | Insert a component into an entity.
-  --
-  -- @since 0.9
-  insert :: EntityID -> b -> m ()
-
-  -- | Lookup a component on an entity.
-  --
-  -- @since 0.9
-  lookup :: (Component a) => EntityID -> m (Maybe a)
-
-  -- | Remove a component from an entity.
-  --
-  -- @since 0.9
-  remove :: (Component a) => EntityID -> m (Maybe a)
-
-  -- | Despawn an entity.
-  --
-  -- @since 0.9
-  despawn :: EntityID -> m ()
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
@@ -17,300 +17,260 @@
 -- 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,
     QueryT (..),
-    QueryReaderF (..),
-    QueryF (..),
-    DynamicQueryReaderF (..),
-    DynamicQueryF (..),
 
-    -- ** Running
-    all,
-    all',
-    single,
-    single',
-    singleMaybe,
-    singleMaybe',
-    map,
-    mapSingle,
-    mapSingleMaybe,
-
-    -- ** Conversion
-    fromReader,
-    toReader,
-    fromDyn,
+    -- ** Operations
+    entity,
+    fetch,
+    fetchMap,
+    fetchMapM,
+    zipFetchMap,
+    zipFetchMapAccum,
+    zipFetchMapM,
+    zipFetchMapAccumM,
 
-    -- * Filters
-    QueryFilter (..),
+    -- ** Filters
     with,
     without,
 
-    -- * Reads and writes
-    ReadsWrites (..),
-    disjoint,
+    -- ** Running
+    query,
+    readSingle,
+    readSingle',
+    readSingleMaybe,
+    readSingleMaybe',
+    querySingle,
+    querySingleMaybe,
+    queryEntities,
+    readQueryEntities,
+
+    -- ** Conversion
+    fromDyn,
   )
 where
 
 import Aztecs.ECS.Component
-import Aztecs.ECS.Query.Class
+import Aztecs.ECS.Entity
 import Aztecs.ECS.Query.Dynamic
-import Aztecs.ECS.Query.Reader (QueryFilter (..), QueryReader (..), with, without)
-import qualified Aztecs.ECS.Query.Reader as QR
-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.Category
 import Control.Monad.Identity
-import Control.Monad.Writer
-import Data.Set (Set)
-import qualified Data.Set as Set
 import GHC.Stack
-import Prelude hiding (all, id, map, reads, (.))
 
--- | @since 0.10
+-- | @since 0.11
 type Query = QueryT Identity
 
 -- | Query for matching entities.
 --
--- @since 0.10
+-- @since 0.11
 newtype QueryT f a = Query
   { -- | Run a query, producing a `DynamicQueryT`.
     --
-    -- @since 0.10
-    runQuery :: Components -> (ReadsWrites, Components, DynamicQueryT f a)
+    -- @since 0.11
+    runQuery :: Components -> (Components, DynamicQueryT f a)
   }
   deriving (Functor)
 
--- | @since 0.10
+-- | @since 0.11
 instance (Applicative f) => Applicative (QueryT f) where
   {-# INLINE pure #-}
-  pure a = Query (mempty,,pure a)
+  pure a = Query (,pure a)
 
   {-# INLINE (<*>) #-}
   (Query f) <*> (Query g) = Query $ \cs ->
-    let !(cIdsG, cs', aQS) = g cs
-        !(cIdsF, cs'', bQS) = f cs'
-     in (cIdsG <> cIdsF, cs'', bQS <*> aQS)
-
--- | @since 0.10
-instance (Applicative f) => QueryReaderF (QueryT f) where
-  {-# INLINE fetch #-}
-  fetch = fromReader fetch
-
-  {-# INLINE fetchMaybe #-}
-  fetchMaybe = fromReader fetchMaybe
-
--- | @since 0.10
-instance (Applicative f) => DynamicQueryReaderF (QueryT f) where
-  {-# INLINE entity #-}
-  entity = fromReader entity
-
-  {-# INLINE fetchDyn #-}
-  fetchDyn = fromReader . fetchDyn
-
-  {-# INLINE fetchMaybeDyn #-}
-  fetchMaybeDyn = fromReader . fetchMaybeDyn
+    let !(cs', aQS) = g cs
+        !(cs'', bQS) = f cs'
+     in (cs'', bQS <*> aQS)
 
--- | @since 0.10
-instance (Applicative f) => DynamicQueryF f (QueryT f) where
-  {-# INLINE adjustDyn #-}
-  adjustDyn f = fromDynInternal $ adjustDyn f
+-- | Fetch the current `EntityID`.
+--
+-- @since 0.11
+{-# INLINE entity #-}
+entity :: QueryT f EntityID
+entity = Query (,entityDyn)
 
-  {-# INLINE adjustDyn_ #-}
-  adjustDyn_ f = fromDynInternal $ adjustDyn_ f
+-- | Fetch a component.
+--
+-- @since 0.11
+{-# INLINE fetch #-}
+fetch :: forall f a. (Component a) => QueryT f a
+fetch = fromDynInternal @f @a $ fetchDyn
 
-  {-# INLINE adjustDynM #-}
-  adjustDynM f = fromDynInternal $ adjustDynM f
+-- | Fetch a component and map it, storing the result.
+--
+-- @since 0.11
+{-# INLINE fetchMap #-}
+fetchMap :: forall m a. (Component a) => (a -> a) -> QueryT m a
+fetchMap f = fromDynInternal @_ @a $ fetchMapDyn f
 
-  {-# INLINE setDyn #-}
-  setDyn = fromDynInternal setDyn
+-- | Fetch a component and map it with a monadic function, storing the result.
+--
+-- @since 0.11
+{-# INLINE fetchMapM #-}
+fetchMapM :: forall m a. (Monad m, Component a) => (a -> m a) -> QueryT m a
+fetchMapM f = fromDynInternal @_ @a $ fetchMapDynM f
 
--- | @since 0.9
-instance (Monad m) => QueryF m (QueryT m) where
-  {-# INLINE adjust #-}
-  adjust :: forall a b. (Component a) => (b -> a -> a) -> QueryT m b -> QueryT m a
-  adjust f = fromWriterInternal @a $ adjustDyn f
+-- | Fetch a component and map it with some input, storing the result.
+--
+-- @since 0.11
+{-# INLINE zipFetchMap #-}
+zipFetchMap :: forall m a b. (Component a) => (b -> a -> a) -> QueryT m b -> QueryT m a
+zipFetchMap f = fromWriterInternal @a $ zipFetchMapDyn f
 
-  {-# INLINE adjust_ #-}
-  adjust_ :: forall a b. (Component a) => (b -> a -> a) -> QueryT m b -> QueryT m ()
-  adjust_ f = fromWriterInternal @a $ adjustDyn_ f
+-- | Fetch a component and map it with some input, storing the result and returning some output.
+--
+-- @since 0.11
+{-# INLINE zipFetchMapAccum #-}
+zipFetchMapAccum ::
+  forall m a b c. (Component a) => (b -> a -> (c, a)) -> QueryT m b -> QueryT m (c, a)
+zipFetchMapAccum f = fromWriterInternal @a $ zipFetchMapAccumDyn f
 
-  {-# INLINE adjustM #-}
-  adjustM :: forall a b. (Component a, Monad m) => (b -> a -> m a) -> QueryT m b -> QueryT m a
-  adjustM f = fromWriterInternal @a $ adjustDynM f
+-- | Fetch a component and map it with some input and a monadic function, storing the result.
+--
+-- @since 0.11
+{-# INLINE zipFetchMapM #-}
+zipFetchMapM :: forall m a b. (Monad m, Component a) => (b -> a -> m a) -> QueryT m b -> QueryT m a
+zipFetchMapM f = fromWriterInternal @a $ zipFetchMapDynM f
 
-  {-# INLINE set #-}
-  set :: forall a. (Component a) => QueryT m a -> QueryT m a
-  set = fromWriterInternal @a setDyn
+-- | Fetch a component and map it with some input and a monadic function,
+-- storing the result and returning some output.
+--
+-- @since 0.11
+{-# INLINE zipFetchMapAccumM #-}
+zipFetchMapAccumM ::
+  forall m a b c. (Monad m, Component a) => (b -> a -> m (c, a)) -> QueryT m b -> QueryT m (c, a)
+zipFetchMapAccumM f = fromWriterInternal @a $ zipFetchMapAccumDynM f
 
--- | Convert a `QueryReader` to a `Query`.
+-- | Filter for entities with a component.
 --
--- @since 0.9
-{-# INLINE fromReader #-}
-fromReader :: (Applicative f) => QueryReader a -> QueryT f a
-fromReader (QueryReader f) = Query $ \cs ->
-  let !(cIds, cs', dynQ) = f cs in (ReadsWrites cIds Set.empty, cs', fromDynReader dynQ)
+-- @since 0.11
+{-# INLINE with #-}
+with :: forall f a. (Component a) => QueryT f ()
+with = fromDynInternal @f @a $ withDyn
 
--- | Convert a `Query` to a `QueryReader`.
+-- | Filter for entities without a component.
 --
--- @since 0.10
-{-# INLINE toReader #-}
-toReader :: Query a -> QueryReader a
-toReader (Query f) = QueryReader $ \cs ->
-  let !(rws, cs', dynQ) = f cs in (reads rws, cs', toDynReader dynQ)
+-- @since 0.11
+{-# INLINE without #-}
+without :: forall f a. (Component a) => QueryT f ()
+without = fromDynInternal @f @a $ withDyn
 
 -- | Convert a `DynamicQueryT` to a `QueryT`.
 --
--- @since 0.10
+-- @since 0.11
 {-# INLINE fromDyn #-}
-fromDyn :: ReadsWrites -> DynamicQueryT f a -> QueryT f a
-fromDyn rws q = Query (rws,,q)
+fromDyn :: DynamicQueryT f a -> QueryT f a
+fromDyn q = Query (,q)
 
 {-# INLINE fromDynInternal #-}
 fromDynInternal ::
-  (ComponentID -> DynamicQueryT f b -> DynamicQueryT f a) ->
-  ComponentID ->
-  QueryT f b ->
-  QueryT f a
-fromDynInternal f cId q = Query $ \cs ->
-  let !(rws, cs', dynQ) = runQuery q cs
-   in (rws <> ReadsWrites Set.empty (Set.singleton cId), cs', f cId dynQ)
+  forall f a b.
+  (Component a) =>
+  (ComponentID -> DynamicQueryT f b) ->
+  QueryT f b
+fromDynInternal f = Query $ \cs ->
+  let !(cId, cs') = CS.insert @a cs in (cs', f cId)
 
 {-# INLINE fromWriterInternal #-}
 fromWriterInternal ::
   forall c f a b.
-  (Applicative f, Component c) =>
+  (Component c) =>
   (ComponentID -> DynamicQueryT f b -> DynamicQueryT f a) ->
   QueryT f b ->
   QueryT f a
 fromWriterInternal f q = Query $ \cs ->
   let !(cId, cs') = CS.insert @c cs
-      !(rws, cs'', dynQ) = runQuery q cs'
-   in (rws <> ReadsWrites Set.empty (Set.singleton cId), cs'', f cId dynQ)
+      !(cs'', dynQ) = runQuery q cs'
+   in (cs'', f cId dynQ)
 
--- | Reads and writes of a `Query`.
+-- | Match and update all entities.
 --
--- @since 0.9
-data ReadsWrites = ReadsWrites
-  { -- | Component IDs being read.
-    --
-    -- @since 0.9
-    reads :: !(Set ComponentID),
-    -- | Component IDs being written.
-    --
-    -- @since 0.9
-    writes :: !(Set ComponentID)
-  }
-  deriving (Show)
-
--- | @since 0.9
-instance Semigroup ReadsWrites where
-  ReadsWrites r1 w1 <> ReadsWrites r2 w2 = ReadsWrites (r1 <> r2) (w1 <> w2)
-
--- | @since 0.9
-instance Monoid ReadsWrites where
-  mempty = ReadsWrites mempty mempty
+-- @since 0.11
+{-# INLINE query #-}
+query :: (Monad m) => QueryT m a -> Entities -> m ([a], Entities)
+query q es = do
+  let !(cs', dynQ) = runQuery q $ components es
+  (as, es') <- queryDyn dynQ es
+  return (as, es' {components = cs'})
 
--- | `True` if the reads and writes of two `Query`s overlap.
+-- | Match and update a single matched entity.
 --
--- @since 0.9
-disjoint :: ReadsWrites -> ReadsWrites -> Bool
-disjoint a b =
-  Set.disjoint (reads a) (writes b)
-    || Set.disjoint (reads b) (writes a)
-    || Set.disjoint (writes b) (writes a)
+-- @since 0.11
+{-# INLINE querySingle #-}
+querySingle :: (HasCallStack, Monad m) => QueryT m a -> Entities -> m (a, Entities)
+querySingle q es = do
+  let !(cs', dynQ) = runQuery q $ components es
+  (as, es') <- mapSingleDyn dynQ es
+  return (as, es' {components = cs'})
 
--- | Match all entities.
+-- | Match and update a single matched entity, or `Nothing`.
 --
--- @since 0.10
-{-# INLINE all #-}
-all :: Query a -> Entities -> ([a], Entities)
-all = QR.all . toReader
+-- @since 0.11
+{-# INLINE querySingleMaybe #-}
+querySingleMaybe :: (Monad m) => QueryT m a -> Entities -> m (Maybe a, Entities)
+querySingleMaybe q es = do
+  let !(cs', dynQ) = runQuery q $ components es
+  (as, es') <- mapSingleMaybeDyn dynQ es
+  return (as, es' {components = cs'})
 
--- | Match all entities.
+-- | Match and update the specified entities.
 --
--- @since 0.10
-{-# INLINE all' #-}
-all' :: Query a -> Entities -> ([a], Components)
-all' = QR.all' . toReader
+-- @since 0.11
+queryEntities :: (Monad m) => [EntityID] -> QueryT m a -> Entities -> m ([a], Entities)
+queryEntities eIds q es = do
+  let !(cs', dynQ) = runQuery q $ components es
+  (as, es') <- queryEntitiesDyn eIds dynQ es
+  return (as, es' {components = cs'})
 
 -- | Match a single entity.
 --
--- @since 0.10
-{-# INLINE single #-}
-single :: (HasCallStack) => Query a -> Entities -> (a, Entities)
-single = QR.single . toReader
+-- @since 0.11
+{-# INLINE readSingle #-}
+readSingle :: (HasCallStack, Monad m) => QueryT m a -> Entities -> m (a, Entities)
+readSingle q es = do
+  let !(cs', dynQ) = runQuery q $ components es
+  as <- singleDyn dynQ es
+  return (as, es {components = cs'})
 
 -- | Match a single entity.
 --
--- @since 0.10
-{-# INLINE single' #-}
-single' :: (HasCallStack) => Query a -> Entities -> (a, Components)
-single' = QR.single' . toReader
+-- @since 0.11
+{-# INLINE readSingle' #-}
+readSingle' :: (HasCallStack, Monad m) => QueryT m a -> Entities -> m (a, Components)
+readSingle' q es = do
+  let !(cs', dynQ) = runQuery q $ components es
+  as <- singleDyn dynQ es
+  return (as, cs')
 
 -- | Match a single entity, or `Nothing`.
 --
--- @since 0.10
-{-# INLINE singleMaybe #-}
-singleMaybe :: Query a -> Entities -> (Maybe a, Entities)
-singleMaybe = QR.singleMaybe . toReader
+-- @since 0.11
+{-# INLINE readSingleMaybe #-}
+readSingleMaybe :: (Monad m) => QueryT m a -> Entities -> m (Maybe a, Entities)
+readSingleMaybe q es = do
+  let !(cs', dynQ) = runQuery q $ components es
+  as <- singleMaybeDyn dynQ es
+  return (as, es {components = cs'})
 
 -- | Match a single entity, or `Nothing`.
 --
--- @since 0.10
-{-# INLINE singleMaybe' #-}
-singleMaybe' :: Query a -> Entities -> (Maybe a, Components)
-singleMaybe' = QR.singleMaybe' . toReader
-
--- | Map all matched entities.
---
--- @since 0.10
-{-# INLINE map #-}
-map :: (Monad m) => QueryT m o -> Entities -> m ([o], Entities)
-map q es = do
-  let !(rws, cs', dynQ) = runQuery q $ components es
-      !cIds = reads rws <> writes rws
-  (as, es') <- mapDyn cIds dynQ es
-  return (as, es' {components = cs'})
-
--- | Map a single matched entity.
---
--- @since 0.10
-{-# INLINE mapSingle #-}
-mapSingle :: (HasCallStack, Monad m) => QueryT m a -> Entities -> m (a, Entities)
-mapSingle q es = do
-  let !(rws, cs', dynQ) = runQuery q $ components es
-      !cIds = reads rws <> writes rws
-  (as, es') <- mapSingleDyn cIds dynQ es
-  return (as, es' {components = cs'})
+-- @since 0.11
+{-# INLINE readSingleMaybe' #-}
+readSingleMaybe' :: (Monad m) => QueryT m a -> Entities -> m (Maybe a, Components)
+readSingleMaybe' q es = do
+  let !(cs', dynQ) = runQuery q $ components es
+  as <- singleMaybeDyn dynQ es
+  return (as, cs')
 
--- | Map a single matched entity, or `Nothing`.
+-- | Match the specified entities.
 --
--- @since 0.10
-{-# INLINE mapSingleMaybe #-}
-mapSingleMaybe :: (Monad m) => QueryT m a -> Entities -> m (Maybe a, Entities)
-mapSingleMaybe q es = do
-  let !(rws, cs', dynQ) = runQuery q $ components es
-      !cIds = reads rws <> writes rws
-  (as, es') <- mapSingleMaybeDyn cIds dynQ es
-  return (as, es' {components = cs'})
+-- @since 0.11
+readQueryEntities :: (Monad m) => [EntityID] -> QueryT m a -> Entities -> m ([a], Entities)
+readQueryEntities eIds q es = do
+  let !(cs', dynQ) = runQuery q $ components es
+  as <- readQueryEntitiesDyn eIds dynQ es
+  return (as, es {components = cs'})
diff --git a/src/Aztecs/ECS/Query/Class.hs b/src/Aztecs/ECS/Query/Class.hs
deleted file mode 100644
--- a/src/Aztecs/ECS/Query/Class.hs
+++ /dev/null
@@ -1,41 +0,0 @@
-{-# LANGUAGE AllowAmbiguousTypes #-}
-{-# LANGUAGE FunctionalDependencies #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-
--- |
--- 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 (QueryF (..)) where
-
-import Aztecs.ECS.Component
-import Control.Monad
-
--- | Query functor.
---
--- @since 0.10
-class (Applicative g, Functor f) => QueryF g f | f -> g where
-  -- | Adjust a `Component` by its type.
-  --
-  -- @since 0.10
-  adjust :: (Component a) => (b -> a -> a) -> f b -> f a
-
-  -- | Adjust a `Component` by its type, ignoring any output.
-  --
-  -- @since 0.10
-  adjust_ :: (Component a) => (b -> a -> a) -> f b -> f ()
-  adjust_ f = void . adjust f
-
-  -- | Adjust a `Component` by its type with some `Monad` @m@.
-  --
-  -- @since 0.10
-  adjustM :: (Component a) => (b -> a -> g a) -> f b -> f a
-
-  -- | Set a `Component` by its type.
-  --
-  -- @since 0.10
-  set :: (Component a) => f a -> f 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,8 +1,9 @@
 {-# LANGUAGE ApplicativeDo #-}
 {-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DeriveFunctor #-}
 {-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE GADTs #-}
 {-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE TupleSections #-}
 {-# LANGUAGE TypeFamilies #-}
 
 -- |
@@ -17,143 +18,435 @@
   ( -- * Dynamic queries
     DynamicQuery,
     DynamicQueryT (..),
-    DynamicQueryReaderF (..),
-    DynamicQueryF (..),
 
-    -- ** Conversion
-    fromDynReader,
-    toDynReader,
+    -- ** Operations
+    entityDyn,
+    fetchDyn,
+    fetchMaybeDyn,
+    fetchMapDyn,
+    fetchMapDynM,
+    zipFetchMapDyn,
+    zipFetchMapAccumDyn,
+    zipFetchMapDynM,
+    zipFetchMapAccumDynM,
 
+    -- ** Filters
+    withDyn,
+    withoutDyn,
+
     -- ** Running
-    mapDyn,
-    filterMapDyn,
+    queryDyn,
+    singleDyn,
+    singleMaybeDyn,
+    queryEntitiesDyn,
+    readQueryDyn,
     mapSingleDyn,
     mapSingleMaybeDyn,
+    readQueryEntitiesDyn,
 
-    -- * Dynamic query filters
-    DynamicQueryFilter (..),
+    -- *** Internal
+    QueryFilter (..),
+    queryFilter,
+    runDynQuery,
+    runDynQueryEntities,
+    readDynQuery,
+    readDynQueryEntities,
   )
 where
 
 import Aztecs.ECS.Component
-import Aztecs.ECS.Query.Dynamic.Class
-import Aztecs.ECS.Query.Dynamic.Reader
+import Aztecs.ECS.Entity
 import Aztecs.ECS.World.Archetype (Archetype)
 import qualified Aztecs.ECS.World.Archetype as A
 import Aztecs.ECS.World.Archetypes (ArchetypeID, Node (..))
 import qualified Aztecs.ECS.World.Archetypes as AS
-import Aztecs.ECS.World.Entities (Entities (..))
+import Aztecs.ECS.World.Entities
+import Control.Applicative
 import Control.Monad.Identity
+import Data.Bifunctor
 import Data.Foldable
 import Data.Map (Map)
-import qualified Data.Map as Map
+import qualified Data.Map.Strict as Map
+import Data.Maybe
 import Data.Set (Set)
 import qualified Data.Set as Set
 import GHC.Stack
+import Prelude hiding (reads)
 
+data Operation f a where
+  Entity :: Operation f EntityID
+  Fetch :: (Component a) => !ComponentID -> Operation f a
+  FetchMaybe :: (Component a) => !ComponentID -> Operation f (Maybe a)
+  FetchMap :: (Component a) => !(a -> a) -> !ComponentID -> Operation f a
+  FetchMapM :: (Monad f, Component a) => !(a -> f a) -> !ComponentID -> Operation f a
+  Adjust :: (Component a) => !(b -> a -> (c, a)) -> !ComponentID -> !(DynamicQueryT f b) -> Operation f (c, a)
+  AdjustM :: (Monad f, Component a) => !(b -> a -> f (c, a)) -> !ComponentID -> !(DynamicQueryT f b) -> Operation f (c, a)
+  With :: !ComponentID -> Operation f ()
+  Without :: !ComponentID -> Operation f ()
+
+-- @since 0.9
 type DynamicQuery = DynamicQueryT Identity
 
 -- | Dynamic query for components by ID.
 --
--- @since 0.10
-newtype DynamicQueryT f a
-  = DynamicQuery
-  { -- | Run a dynamic query.
-    --
-    -- @since 0.10
-    runDynQuery :: Archetype -> f ([a], Archetype)
-  }
-  deriving (Functor)
+-- @since 0.11
+data DynamicQueryT f a where
+  Pure :: !a -> DynamicQueryT f a
+  Map :: !(a -> b) -> !(DynamicQueryT f a) -> DynamicQueryT f b
+  Ap :: !(DynamicQueryT f (a -> b)) -> !(DynamicQueryT f a) -> DynamicQueryT f b
+  Op :: !(Operation f a) -> DynamicQueryT f a
 
--- | @since 0.10
-instance (Applicative f) => Applicative (DynamicQueryT f) where
+instance Functor (DynamicQueryT f) where
+  {-# INLINE fmap #-}
+  fmap = Map
+
+-- | @since 0.11
+instance Applicative (DynamicQueryT f) where
   {-# INLINE pure #-}
-  pure a = DynamicQuery $ \arch -> pure (replicate (length $ A.entities arch) a, arch)
+  pure = Pure
 
   {-# INLINE (<*>) #-}
-  f <*> g = DynamicQuery $ \arch -> do
-    x <- runDynQuery g arch
-    y <- runDynQuery f arch
-    return $
-      let (as, arch') = x
-          (bs, arch'') = y
-       in (zipWith ($) bs as, arch' <> arch'')
+  (<*>) = Ap
 
--- | @since 0.10
-instance DynamicQueryReaderF DynamicQuery where
-  {-# INLINE entity #-}
-  entity = fromDynReader entity
+{-# INLINE entityDyn #-}
+entityDyn :: DynamicQueryT f EntityID
+entityDyn = Op Entity
 
-  {-# INLINE fetchDyn #-}
-  fetchDyn = fromDynReader . fetchDyn
+{-# INLINE fetchDyn #-}
+fetchDyn :: (Component a) => ComponentID -> DynamicQueryT f a
+fetchDyn = Op . Fetch
 
-  {-# INLINE fetchMaybeDyn #-}
-  fetchMaybeDyn = fromDynReader . fetchMaybeDyn
+{-# INLINE fetchMaybeDyn #-}
+fetchMaybeDyn :: (Component a) => ComponentID -> DynamicQueryT f (Maybe a)
+fetchMaybeDyn = Op . FetchMaybe
 
--- | @since 0.10
-instance (Applicative f) => DynamicQueryF f (DynamicQueryT f) where
-  {-# INLINE adjustDyn #-}
-  adjustDyn f cId q =
-    DynamicQuery (fmap (\(bs, arch') -> A.zipWith bs f cId arch') . runDynQuery q)
+{-# INLINE fetchMapDyn #-}
+fetchMapDyn :: (Component a) => (a -> a) -> ComponentID -> DynamicQueryT f a
+fetchMapDyn f = Op . FetchMap f
 
-  {-# INLINE adjustDyn_ #-}
-  adjustDyn_ f cId q = DynamicQuery $ \arch ->
-    fmap (\(bs, arch') -> (map (const ()) bs, A.zipWith_ bs f cId arch')) (runDynQuery q arch)
+{-# INLINE fetchMapDynM #-}
+fetchMapDynM :: (Monad f, Component a) => (a -> f a) -> ComponentID -> DynamicQueryT f a
+fetchMapDynM f = Op . FetchMapM f
 
-  {-# INLINE adjustDynM #-}
-  adjustDynM f cId q = DynamicQuery $ \arch -> do
-    (bs, arch') <- runDynQuery q arch
-    A.zipWithM bs f cId arch'
+{-# INLINE zipFetchMapDyn #-}
+zipFetchMapDyn :: (Component a) => (b -> a -> a) -> ComponentID -> DynamicQueryT f b -> DynamicQueryT f a
+zipFetchMapDyn f cId q = snd <$> Op (Adjust (\b a -> ((), f b a)) cId q)
 
-  {-# INLINE setDyn #-}
-  setDyn cId q =
-    DynamicQuery (fmap (\(bs, arch') -> (bs, A.insertAscList cId bs arch')) . runDynQuery q)
+{-# INLINE zipFetchMapAccumDyn #-}
+zipFetchMapAccumDyn :: (Component a) => (b -> a -> (c, a)) -> ComponentID -> DynamicQueryT f b -> DynamicQueryT f (c, a)
+zipFetchMapAccumDyn f cId q = Op $ Adjust f cId q
 
--- | Convert a `DynamicQueryReaderT` to a `DynamicQueryT`.
---
--- @since 0.10
-{-# INLINE fromDynReader #-}
-fromDynReader :: (Applicative m) => DynamicQueryReader a -> DynamicQueryT m a
-fromDynReader q = DynamicQuery $ \arch -> let !os = runDynQueryReader q arch in pure (os, arch)
+{-# INLINE zipFetchMapDynM #-}
+zipFetchMapDynM :: (Monad f, Component a) => (b -> a -> f a) -> ComponentID -> DynamicQueryT f b -> DynamicQueryT f a
+zipFetchMapDynM f cId q = snd <$> zipFetchMapAccumDynM (\b a -> ((),) <$> f b a) cId q
 
--- | Convert a `DynamicQueryT` to a `DynamicQueryReaderT`.
---
--- @since 0.10
-{-# INLINE toDynReader #-}
-toDynReader :: DynamicQuery a -> DynamicQueryReader a
-toDynReader q = DynamicQueryReader $ \arch -> fst $ runIdentity $ runDynQuery q arch
+{-# INLINE zipFetchMapAccumDynM #-}
+zipFetchMapAccumDynM :: (Monad f, Component a) => (b -> a -> f (c, a)) -> ComponentID -> DynamicQueryT f b -> DynamicQueryT f (c, a)
+zipFetchMapAccumDynM f cId q = Op $ AdjustM f cId q
 
--- | Map all matched entities.
---
--- @since 0.10
-{-# INLINE mapDyn #-}
-mapDyn :: (Monad m) => Set ComponentID -> DynamicQueryT m a -> Entities -> m ([a], Entities)
-mapDyn cIds = mapDyn' cIds id
+{-# INLINE withDyn #-}
+withDyn :: ComponentID -> DynamicQueryT f ()
+withDyn = Op . With
 
--- | Map all matched entities with a filter.
---
--- @since 0.10
-{-# INLINE filterMapDyn #-}
-filterMapDyn ::
+{-# INLINE withoutDyn #-}
+withoutDyn :: ComponentID -> DynamicQueryT f ()
+withoutDyn = Op . Without
+
+{-# INLINE opFilter #-}
+opFilter :: Operation f a -> QueryFilter
+opFilter Entity = mempty
+opFilter (Fetch cId) = mempty {filterWith = Set.singleton cId}
+opFilter (FetchMaybe cId) = mempty {filterWith = Set.singleton cId}
+opFilter (FetchMap _ cId) = mempty {filterWith = Set.singleton cId}
+opFilter (FetchMapM _ cId) = mempty {filterWith = Set.singleton cId}
+opFilter (Adjust _ cId q) = queryFilter q <> mempty {filterWith = Set.singleton cId}
+opFilter (AdjustM _ cId q) = queryFilter q <> mempty {filterWith = Set.singleton cId}
+opFilter (With cId) = mempty {filterWith = Set.singleton cId}
+opFilter (Without cId) = mempty {filterWithout = Set.singleton cId}
+
+{-# INLINE queryFilter #-}
+queryFilter :: DynamicQueryT f a -> QueryFilter
+queryFilter (Pure _) = mempty
+queryFilter (Map _ q) = queryFilter q
+queryFilter (Ap f g) = queryFilter f <> queryFilter g
+queryFilter (Op op) = opFilter op
+
+{-# INLINE runOp #-}
+runOp :: (Applicative f) => Operation f a -> Archetype -> f ([a], Archetype)
+runOp Entity arch = pure (Set.toList $ A.entities arch, mempty)
+runOp (Fetch cId) arch = pure (A.lookupComponentsAsc cId arch, mempty)
+runOp (FetchMaybe cId) arch =
+  pure
+    ( case A.lookupComponentsAscMaybe cId arch of
+        Just as -> fmap Just as
+        Nothing -> replicate (length $ A.entities arch) Nothing,
+      mempty
+    )
+runOp (FetchMap f cId) arch = pure $ A.map f cId arch
+runOp (FetchMapM f cId) arch = do
+  (as, arch') <- A.mapM f cId arch
+  return (as, arch')
+runOp (Adjust f cId q) arch = do
+  res <- runDynQuery q arch
+  return $
+    let !(bs, arch') = res
+        !(as, arch'') = A.zipWith bs f cId arch
+     in (as, arch'' <> arch')
+runOp (AdjustM f cId q) arch = do
+  (as, arch') <- runDynQuery q arch
+  (bs, arch'') <- A.zipWithM as f cId arch
+  return (bs, arch'' <> arch')
+runOp (With _) _ = pure ([], mempty)
+runOp (Without _) _ = pure ([], mempty)
+
+{-# INLINE readOp #-}
+readOp :: (Applicative f) => Operation f a -> Archetype -> f [a]
+readOp Entity arch = pure $ Set.toList $ A.entities arch
+readOp (Fetch cId) arch = pure $ A.lookupComponentsAsc cId arch
+readOp (FetchMaybe cId) arch =
+  pure $
+    case A.lookupComponentsAscMaybe cId arch of
+      Just as -> fmap Just as
+      Nothing -> replicate (length $ A.entities arch) Nothing
+readOp (FetchMap f cId) arch = do
+  bs <- readOp (Fetch cId) arch
+  return $ map f bs
+readOp (FetchMapM f cId) arch = do
+  bs <- readOp (Fetch cId) arch
+  mapM f bs
+readOp (Adjust f cId q) arch = do
+  as <- readDynQuery q arch
+  bs <- readOp (Fetch cId) arch
+  return $ zipWith f as bs
+readOp (AdjustM f cId q) arch = do
+  as <- readDynQuery q arch
+  bs <- readOp (Fetch cId) arch
+  zipWithM f as bs
+readOp (With _) _ = pure []
+readOp (Without _) _ = pure []
+
+{-# INLINE queryEntitiesDyn #-}
+queryEntitiesDyn ::
   (Monad m) =>
-  Set ComponentID ->
-  (Node -> Bool) ->
+  [EntityID] ->
   DynamicQueryT m a ->
   Entities ->
   m ([a], Entities)
-filterMapDyn cIds f = mapDyn' cIds (Map.filter f)
+queryEntitiesDyn eIds q es =
+  let qf = queryFilter q
+      go = runDynQueryEntities eIds q
+   in if Set.null $ filterWith qf
+        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 n' = n {nodeArchetype = arch' <> nodeArchetype n}
+                    !nodes = Map.insert aId n' . AS.nodes $ archetypes esAcc
+                return (as' ++ acc, esAcc {archetypes = (archetypes esAcc) {AS.nodes = nodes}})
+           in foldlM go' ([], es) $ Map.toList . AS.find (filterWith qf) (filterWithout qf) $ archetypes es
 
+readQueryEntitiesDyn :: (Monad m) => [EntityID] -> DynamicQueryT m a -> Entities -> m [a]
+readQueryEntitiesDyn eIds q es =
+  let qf = queryFilter q
+   in if Set.null $ filterWith qf
+        then readDynQueryEntities eIds q A.empty {A.entities = Map.keysSet $ entities es}
+        else
+          let go n = readDynQuery q $ AS.nodeArchetype n
+           in concat <$> mapM go (AS.find (filterWith qf) (filterWithout qf) $ archetypes es)
+
+runOpEntities :: (Applicative f) => Operation f a -> [EntityID] -> Archetype -> f ([a], Archetype)
+runOpEntities Entity es _ = pure (es, mempty)
+runOpEntities (Fetch cId) es arch =
+  pure
+    ( map snd
+        . filter (\(e, _) -> e `elem` es)
+        . Map.toList
+        $ A.lookupComponents cId arch,
+      mempty
+    )
+runOpEntities (FetchMaybe cId) es arch =
+  pure
+    ( map (\(e, a) -> if e `elem` es then Just a else Nothing)
+        . Map.toList
+        $ A.lookupComponents cId arch,
+      mempty
+    )
+runOpEntities (FetchMap f cId) es arch =
+  pure $
+    let go e a =
+          if e `elem` es
+            then let a' = f a in (Just a', a')
+            else (Nothing, a)
+        !(as, arch') = A.zipWith es go cId arch
+     in (mapMaybe fst as, arch')
+runOpEntities (FetchMapM f cId) es arch = do
+  (as, arch') <- runOpEntities (AdjustM (\() a -> (,a) <$> f a) cId (pure ())) es arch
+  return (map snd as, arch')
+runOpEntities (Adjust f cId q) es arch = do
+  res <- runDynQuery q arch
+  return $
+    let go (e, b) a =
+          if e `elem` es
+            then let (x, y) = f b a in (Just x, y)
+            else (Nothing, a)
+        !(bs, arch') = res
+        !(as, arch'') = A.zipWith (zip es bs) go cId arch
+     in (mapMaybe (\(m, b) -> fmap (,b) m) as, arch'' <> arch')
+runOpEntities (AdjustM f cId q) es arch = do
+  (bs, arch') <- runDynQuery q arch
+  let go (e, b) a =
+        if e `elem` es
+          then do
+            (x, y) <- f b a
+            return (Just x, y)
+          else return (Nothing, a)
+  (as, arch'') <- A.zipWithM (zip es bs) go cId arch
+  return (mapMaybe (\(m, b) -> fmap (,b) m) as, arch'' <> arch')
+runOpEntities (With _) _ arch = pure ([], arch)
+runOpEntities (Without _) _ arch = pure ([], arch)
+
+runDynQueryEntities :: (Applicative f) => [EntityID] -> DynamicQueryT f a -> Archetype -> f ([a], Archetype)
+runDynQueryEntities es (Pure a) _ = pure (replicate (length es) a, mempty)
+runDynQueryEntities es (Map f q) arch = first (fmap f) <$> runDynQueryEntities es q arch
+runDynQueryEntities es (Ap f g) arch = do
+  res <- runDynQueryEntities es g arch
+  res' <- runDynQueryEntities es f arch
+  return $
+    let (as, arch') = res
+        (bs, arch'') = res'
+     in (zipWith ($) bs as, arch'' <> arch')
+runDynQueryEntities es (Op op) arch = runOpEntities op es arch
+
+{-# INLINE readOpEntities #-}
+readOpEntities :: (Applicative f) => Operation f a -> [EntityID] -> Archetype -> f [a]
+readOpEntities Entity es _ = pure es
+readOpEntities (Fetch cId) es arch =
+  pure
+    . map snd
+    . filter (\(e, _) -> e `elem` es)
+    . Map.toList
+    $ A.lookupComponents cId arch
+readOpEntities (FetchMaybe cId) es arch =
+  pure
+    . map (\(e, a) -> if e `elem` es then Just a else Nothing)
+    . Map.toList
+    $ A.lookupComponents cId arch
+readOpEntities (FetchMap f cId) es arch = do
+  b <- readOpEntities (Fetch cId) es arch
+  pure $ map f b
+readOpEntities (FetchMapM f cId) es arch = do
+  b <- readOpEntities (Fetch cId) es arch
+  mapM f b
+readOpEntities (Adjust f cId q) es arch = do
+  a <- readDynQueryEntities es q arch
+  b <- readOpEntities (Fetch cId) es arch
+  pure $ zipWith f a b
+readOpEntities (AdjustM f cId q) es arch = do
+  a <- readDynQueryEntities es q arch
+  b <- readOpEntities (Fetch cId) es arch
+  zipWithM f a b
+readOpEntities (With _) _ _ = pure []
+readOpEntities (Without _) _ _ = pure []
+
+{-# INLINE readDynQueryEntities #-}
+readDynQueryEntities :: (Applicative f) => [EntityID] -> DynamicQueryT f a -> Archetype -> f [a]
+readDynQueryEntities es (Pure a) _ = pure $ replicate (length es) a
+readDynQueryEntities es (Map f q) arch = fmap f <$> readDynQueryEntities es q arch
+readDynQueryEntities es (Ap f g) arch = do
+  a <- readDynQueryEntities es g arch
+  b <- readDynQueryEntities es f arch
+  pure $ b <*> a
+readDynQueryEntities es (Op op) arch = readOpEntities op es arch
+
+{-# INLINE runDynQuery #-}
+runDynQuery :: (Applicative f) => DynamicQueryT f a -> Archetype -> f ([a], Archetype)
+runDynQuery (Pure a) arch = pure (replicate (length $ A.entities arch) a, mempty)
+runDynQuery (Map f q) arch = do
+  res <- runDynQuery q arch
+  return $ first (fmap f) res
+runDynQuery (Ap f g) arch = do
+  res <- runDynQuery g arch
+  res' <- runDynQuery f arch
+  return $
+    let (as, arch') = res
+        (bs, arch'') = res'
+     in (zipWith ($) bs as, arch'' <> arch')
+runDynQuery (Op op) arch = runOp op arch
+
+{-# INLINE readDynQuery #-}
+readDynQuery :: (Applicative f) => DynamicQueryT f a -> Archetype -> f [a]
+readDynQuery (Pure a) arch = pure $ replicate (length $ A.entities arch) a
+readDynQuery (Map f q) arch = fmap f <$> readDynQuery q arch
+readDynQuery (Ap f g) arch = do
+  as <- readDynQuery g arch
+  bs <- readDynQuery f arch
+  pure $ zipWith ($) bs as
+readDynQuery (Op op) arch = readOp op arch
+
+-- | Match all entities.
+--
+-- @since 0.11
+readQueryDyn :: (Monad m) => DynamicQueryT m a -> Entities -> m [a]
+readQueryDyn q es =
+  let qf = queryFilter q
+   in if Set.null $ filterWith qf
+        then readDynQuery q $ A.empty {A.entities = Map.keysSet $ entities es}
+        else
+          let go n = readDynQuery q $ AS.nodeArchetype n
+           in concat <$> mapM go (AS.find (filterWith qf) (filterWithout qf) $ archetypes es)
+
+-- | Match a single entity.
+--
+-- @since 0.11
+singleDyn :: (HasCallStack, Monad m) => DynamicQueryT m a -> Entities -> m a
+singleDyn q es = do
+  res <- singleMaybeDyn q es
+  return $ case res of
+    Just a -> a
+    _ -> error "singleDyn: expected a single entity"
+
+-- | Match a single entity, or `Nothing`.
+--
+-- @since 0.11
+singleMaybeDyn :: (Monad m) => DynamicQueryT m a -> Entities -> m (Maybe a)
+singleMaybeDyn q es =
+  let qf = queryFilter q
+   in if Set.null $ filterWith qf
+        then case Map.keys $ entities es of
+          [eId] -> do
+            res <- readDynQuery q $ A.singleton eId
+            return $ case res of
+              [a] -> Just a
+              _ -> Nothing
+          _ -> return Nothing
+        else case Map.elems $ AS.find (filterWith qf) (filterWithout qf) $ archetypes es of
+          [n] -> do
+            res <- readDynQuery q $ AS.nodeArchetype n
+            return $ case res of
+              [a] -> Just a
+              _ -> Nothing
+          _ -> return Nothing
+
+-- | Match and update all matched entities.
+--
+-- @since 0.11
+{-# INLINE queryDyn #-}
+queryDyn :: (Monad m) => DynamicQueryT m a -> Entities -> m ([a], Entities)
+queryDyn = mapDyn' id
+
 {-# INLINE mapDyn' #-}
 mapDyn' ::
   (Monad m) =>
-  Set ComponentID ->
   (Map ArchetypeID Node -> Map ArchetypeID Node) ->
   DynamicQueryT m a ->
   Entities ->
   m ([a], Entities)
-mapDyn' cIds f q es =
-  let go = runDynQuery q
-   in if Set.null cIds
+mapDyn' f q es =
+  let !qf = queryFilter q
+      go = runDynQuery q
+   in if Set.null $ filterWith qf
         then do
           (as, _) <- go A.empty {A.entities = Map.keysSet $ entities es}
           return (as, es)
@@ -163,38 +456,56 @@
                 let n' = n {nodeArchetype = arch' <> nodeArchetype n}
                     !nodes = Map.insert aId n' . AS.nodes $ archetypes esAcc
                 return (as' ++ acc, esAcc {archetypes = (archetypes esAcc) {AS.nodes = nodes}})
-           in foldlM go' ([], es) $ Map.toList . f . AS.find cIds $ archetypes es
+           in foldlM go' ([], es) $ Map.toList . f . AS.find (filterWith qf) (filterWithout qf) $ archetypes es
 
 -- | Map a single matched entity.
 --
--- @since 0.10
-mapSingleDyn :: (HasCallStack, Monad m) => Set ComponentID -> DynamicQueryT m a -> Entities -> m (a, Entities)
-mapSingleDyn cIds q es = do
-  res <- mapSingleMaybeDyn cIds q es
+-- @since 0.11
+mapSingleDyn :: (HasCallStack, Monad m) => DynamicQueryT m a -> Entities -> m (a, Entities)
+mapSingleDyn q es = do
+  (res, es') <- mapSingleMaybeDyn q es
   return $ case res of
-    (Just a, es') -> (a, es')
+    Just a -> (a, es')
     _ -> error "mapSingleDyn: expected single matching entity"
 
 -- | Map a single matched entity, or @Nothing@.
 --
--- @since 0.10
+-- @since 0.11
 {-# INLINE mapSingleMaybeDyn #-}
-mapSingleMaybeDyn :: (Monad m) => Set ComponentID -> DynamicQueryT m a -> Entities -> m (Maybe a, Entities)
-mapSingleMaybeDyn cIds q es =
-  if Set.null cIds
-    then case Map.keys $ entities es of
-      [eId] -> do
-        res <- runDynQuery q $ 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 $ AS.nodeArchetype n
-        return $ case res of
-          ([a], arch') ->
-            let nodes = Map.insert aId n {nodeArchetype = arch' <> nodeArchetype n} . AS.nodes $ archetypes es
-             in (Just a, es {archetypes = (archetypes es) {AS.nodes = nodes}})
-          _ -> (Nothing, es)
-      _ -> pure (Nothing, es)
+mapSingleMaybeDyn :: (Monad m) => DynamicQueryT m a -> Entities -> m (Maybe a, Entities)
+mapSingleMaybeDyn q es =
+  let qf = queryFilter q
+   in if Set.null $ filterWith qf
+        then case Map.keys $ entities es of
+          [eId] -> do
+            res <- runDynQuery q $ A.singleton eId
+            return $ case res of
+              ([a], _) -> (Just a, es)
+              _ -> (Nothing, es)
+          _ -> pure (Nothing, es)
+        else case Map.toList $ AS.find (filterWith qf) (filterWithout qf) $ archetypes es of
+          [(aId, n)] -> do
+            (as, arch') <- runDynQuery q $ AS.nodeArchetype n
+            return $ case as of
+              [a] ->
+                let nodes = Map.insert aId n {nodeArchetype = arch' <> nodeArchetype n} . AS.nodes $ archetypes es
+                 in (Just a, es {archetypes = (archetypes es) {AS.nodes = nodes}})
+              _ -> (Nothing, es)
+          _ -> pure (Nothing, es)
+
+-- | `Query` filter.
+--
+-- @since 0.11
+data QueryFilter = QueryFilter
+  { filterWith :: !(Set ComponentID),
+    filterWithout :: !(Set ComponentID)
+  }
+  deriving (Show)
+
+-- | @since 0.9
+instance Semigroup QueryFilter where
+  QueryFilter r1 w1 <> QueryFilter r2 w2 = QueryFilter (r1 <> r2) (w1 <> w2)
+
+-- | @since 0.9
+instance Monoid QueryFilter where
+  mempty = QueryFilter mempty mempty
diff --git a/src/Aztecs/ECS/Query/Dynamic/Class.hs b/src/Aztecs/ECS/Query/Dynamic/Class.hs
deleted file mode 100644
--- a/src/Aztecs/ECS/Query/Dynamic/Class.hs
+++ /dev/null
@@ -1,39 +0,0 @@
-{-# LANGUAGE FunctionalDependencies #-}
-
--- |
--- 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 (DynamicQueryF (..)) where
-
-import Aztecs.ECS.Component
-import Control.Monad
-
--- | Dynamic query functor.
---
--- @since 0.10
-class (Applicative m, Functor f) => DynamicQueryF m f | f -> m where
-  -- | Adjust a `Component` by its `ComponentID`.
-  --
-  -- @since 0.10
-  adjustDyn :: (Component a) => (b -> a -> a) -> ComponentID -> f b -> f a
-
-  -- | Adjust a `Component` by its `ComponentID`, ignoring any output.
-  --
-  -- @since 0.10
-  adjustDyn_ :: (Component a) => (b -> a -> a) -> ComponentID -> f b -> f ()
-  adjustDyn_ f cId = void . adjustDyn f cId
-
-  -- | Adjust a `Component` by its `ComponentID` with some applicative functor @g@.
-  --
-  -- @since 0.10
-  adjustDynM :: (Monad m, Component a) => (b -> a -> m a) -> ComponentID -> f b -> f a
-
-  -- | Set a `Component` by its `ComponentID`.
-  --
-  -- @since 0.10
-  setDyn :: (Component a) => ComponentID -> f a -> f a
diff --git a/src/Aztecs/ECS/Query/Dynamic/Reader.hs b/src/Aztecs/ECS/Query/Dynamic/Reader.hs
deleted file mode 100644
--- a/src/Aztecs/ECS/Query/Dynamic/Reader.hs
+++ /dev/null
@@ -1,146 +0,0 @@
-{-# LANGUAGE DeriveFunctor #-}
-{-# 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 (..),
-    DynamicQueryReaderF (..),
-
-    -- ** Running
-    allDyn,
-    filterDyn,
-    singleDyn,
-    singleMaybeDyn,
-
-    -- * Dynamic query filters
-    DynamicQueryFilter (..),
-  )
-where
-
-import Aztecs.ECS.Component
-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 Data.Map as Map
-import Data.Set (Set)
-import qualified Data.Set as Set
-import GHC.Stack
-
--- | Dynamic query reader.
---
--- @since 0.10
-newtype DynamicQueryReader a = DynamicQueryReader
-  { -- | Run a dynamic query reader.
-    --
-    -- @since 0.10
-    runDynQueryReader :: Archetype -> [a]
-  }
-  deriving (Functor)
-
--- | @since 0.10
-instance Applicative DynamicQueryReader where
-  {-# INLINE pure #-}
-  pure a = DynamicQueryReader $ \arch -> replicate (length $ A.entities arch) a
-
-  {-# INLINE (<*>) #-}
-  f <*> g = DynamicQueryReader $ \arch ->
-    zipWith ($) (runDynQueryReader f arch) $ runDynQueryReader g arch
-
--- | @since 0.10
-instance DynamicQueryReaderF DynamicQueryReader where
-  {-# INLINE entity #-}
-  entity = DynamicQueryReader $ \arch -> Set.toList $ A.entities arch
-
-  {-# INLINE fetchDyn #-}
-  fetchDyn cId = DynamicQueryReader $ \arch -> A.lookupComponentsAsc cId arch
-
-  {-# INLINE fetchMaybeDyn #-}
-  fetchMaybeDyn cId = DynamicQueryReader $ \arch -> case A.lookupComponentsAscMaybe cId arch of
-    Just as -> fmap Just as
-    Nothing -> replicate (length $ A.entities arch) Nothing
-
--- | Dynamic query for components by ID.
---
--- @since 0.9
-
--- | Dynamic query filter.
---
--- @since 0.9
-data DynamicQueryFilter = DynamicQueryFilter
-  { -- | `ComponentID`s to include.
-    --
-    -- @since 0.9
-    filterWith :: !(Set ComponentID),
-    -- | `ComponentID`s to exclude.
-    --
-    -- @since 0.9
-    filterWithout :: !(Set ComponentID)
-  }
-
--- | @since 0.9
-instance Semigroup DynamicQueryFilter where
-  DynamicQueryFilter withA withoutA <> DynamicQueryFilter withB withoutB =
-    DynamicQueryFilter (withA <> withB) (withoutA <> withoutB)
-
--- | @since 0.9
-instance Monoid DynamicQueryFilter where
-  mempty = DynamicQueryFilter mempty mempty
-
--- | Match all entities.
---
--- @since 0.10
-allDyn :: Set ComponentID -> DynamicQueryReader a -> Entities -> [a]
-allDyn cIds q es =
-  if Set.null cIds
-    then runDynQueryReader q A.empty {A.entities = Map.keysSet $ entities es}
-    else
-      let go n = runDynQueryReader q $ AS.nodeArchetype n
-       in concatMap go (AS.find cIds $ archetypes es)
-
--- | Match all entities with a filter.
---
--- @since 0.10
-filterDyn :: Set ComponentID -> (Node -> Bool) -> DynamicQueryReader a -> Entities -> [a]
-filterDyn cIds f q es =
-  if Set.null cIds
-    then runDynQueryReader q A.empty {A.entities = Map.keysSet $ entities es}
-    else
-      let go n = runDynQueryReader q $ AS.nodeArchetype n
-       in concatMap go (Map.filter f $ AS.find cIds $ archetypes es)
-
--- | Match a single entity.
---
--- @since 0.10
-singleDyn :: (HasCallStack) => Set ComponentID -> DynamicQueryReader a -> Entities -> a
-singleDyn cIds q es = case singleMaybeDyn cIds q es of
-  Just a -> a
-  _ -> error "singleDyn: expected a single entity"
-
--- | Match a single entity, or `Nothing`.
---
--- @since 0.10
-singleMaybeDyn :: Set ComponentID -> DynamicQueryReader a -> Entities -> Maybe a
-singleMaybeDyn cIds q es =
-  if Set.null cIds
-    then case Map.keys $ entities es of
-      [eId] -> case runDynQueryReader q $ A.singleton eId of
-        [a] -> Just a
-        _ -> Nothing
-      _ -> Nothing
-    else case Map.elems $ AS.find cIds $ archetypes es of
-      [n] -> case runDynQueryReader 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
deleted file mode 100644
--- a/src/Aztecs/ECS/Query/Dynamic/Reader/Class.hs
+++ /dev/null
@@ -1,32 +0,0 @@
--- |
--- 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 (DynamicQueryReaderF (..)) where
-
-import Aztecs.ECS.Component
-import Aztecs.ECS.Entity
-
--- | Dynamic query reader functor.
---
--- @since 0.10
-class (Functor f) => DynamicQueryReaderF f where
-  -- | Fetch the currently matched `EntityID`.
-  --
-  -- @since 0.10
-  entity :: f EntityID
-
-  -- | Fetch a `Component` by its `ComponentID`.
-  --
-  -- @since 0.10
-  fetchDyn :: (Component a) => ComponentID -> f a
-
-  -- | Try to fetch a `Component` by its `ComponentID`.
-  --
-  -- @since 0.10
-  fetchMaybeDyn :: (Component a) => ComponentID -> f (Maybe a)
-  fetchMaybeDyn cId = Just <$> fetchDyn cId
diff --git a/src/Aztecs/ECS/Query/Reader.hs b/src/Aztecs/ECS/Query/Reader.hs
deleted file mode 100644
--- a/src/Aztecs/ECS/Query/Reader.hs
+++ /dev/null
@@ -1,175 +0,0 @@
-{-# 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 (..),
-    QueryReaderF (..),
-    DynamicQueryReaderF (..),
-
-    -- ** Running
-    all,
-    all',
-    single,
-    single',
-    singleMaybe,
-    singleMaybe',
-
-    -- * Filters
-    QueryFilter (..),
-    with,
-    without,
-    DynamicQueryFilter (..),
-  )
-where
-
-import Aztecs.ECS.Component
-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.Monad.Identity
-import Data.Set (Set)
-import qualified Data.Set as Set
-import GHC.Stack
-import Prelude hiding (all)
-
--- | Query to read from entities.
---
--- @since 0.10
-newtype QueryReader a
-  = QueryReader
-  { -- | Run a query reader.
-    --
-    -- @since 0.10
-    runQueryReader :: Components -> (Set ComponentID, Components, DynamicQueryReader a)
-  }
-  deriving (Functor)
-
--- | @since 0.10
-instance Applicative QueryReader where
-  pure a = QueryReader (mempty,,pure a)
-  {-# INLINE pure #-}
-
-  (QueryReader f) <*> (QueryReader g) = QueryReader $ \cs ->
-    let !(cIdsG, cs', aQS) = g cs
-        !(cIdsF, cs'', bQS) = f cs'
-     in (cIdsG <> cIdsF, cs'', bQS <*> aQS)
-  {-# INLINE (<*>) #-}
-
--- | @since 0.10
-instance QueryReaderF QueryReader where
-  fetch :: forall a. (Component a) => QueryReader a
-  fetch = QueryReader $ \cs ->
-    let !(cId, cs') = CS.insert @a cs in (Set.singleton cId, cs', fetchDyn cId)
-  {-# INLINE fetch #-}
-
-  fetchMaybe :: forall a. (Component a) => QueryReader (Maybe a)
-  fetchMaybe = QueryReader $ \cs ->
-    let !(cId, cs') = CS.insert @a cs in (Set.singleton cId, cs', fetchMaybeDyn cId)
-  {-# INLINE fetchMaybe #-}
-
--- | @since 0.10
-instance DynamicQueryReaderF QueryReader 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`.
---
--- @since 0.9
-newtype QueryFilter = QueryFilter
-  { -- | Run a query filter.
-    runQueryFilter :: Components -> (DynamicQueryFilter, Components)
-  }
-
--- | @since 0.9
-instance Semigroup QueryFilter where
-  a <> b =
-    QueryFilter
-      ( \cs ->
-          let !(withA', cs') = runQueryFilter a cs
-              !(withB', cs'') = runQueryFilter b cs'
-           in (withA' <> withB', cs'')
-      )
-
--- | @since 0.9
-instance Monoid QueryFilter where
-  mempty = QueryFilter (mempty,)
-
--- | Filter for entities containing this component.
---
--- @since 0.9
-with :: forall a. (Component a) => QueryFilter
-with = QueryFilter $ \cs ->
-  let !(cId, cs') = CS.insert @a cs in (mempty {filterWith = Set.singleton cId}, cs')
-
--- | Filter out entities containing this component.
---
--- @since 0.9
-without :: forall a. (Component a) => QueryFilter
-without = QueryFilter $ \cs ->
-  let !(cId, cs') = CS.insert @a cs in (mempty {filterWithout = Set.singleton cId}, cs')
-
--- | Match all entities.
---
--- @since 0.10
-{-# INLINE all #-}
-all :: QueryReader a -> Entities -> ([a], Entities)
-all q es = let !(as, cs) = all' q es in (as, es {E.components = cs})
-
--- | Match all entities.
---
--- @since 0.10
-{-# INLINE all' #-}
-all' :: QueryReader a -> Entities -> ([a], Components)
-all' q es = let !(rs, cs', dynQ) = runQueryReader q (E.components es) in (allDyn rs dynQ es, cs')
-
--- | Match a single entity.
---
--- @since 0.10
-{-# INLINE single #-}
-single :: (HasCallStack) => QueryReader a -> Entities -> (a, Entities)
-single q es = let !(a, cs) = single' q es in (a, es {E.components = cs})
-
--- | Match a single entity.
---
--- @since 0.10
-{-# INLINE single' #-}
-single' :: (HasCallStack) => QueryReader a -> Entities -> (a, Components)
-single' q es = let !(rs, cs', dynQ) = runQueryReader q (E.components es) in (singleDyn rs dynQ es, cs')
-
--- | Match a single entity.
---
--- @since 0.10
-{-# INLINE singleMaybe #-}
-singleMaybe :: QueryReader a -> Entities -> (Maybe a, Entities)
-singleMaybe q es = let !(a, cs) = singleMaybe' q es in (a, es {E.components = cs})
-
--- | Match a single entity.
---
--- @since 0.10
-{-# INLINE singleMaybe' #-}
-singleMaybe' :: QueryReader a -> Entities -> (Maybe a, Components)
-singleMaybe' q es = let !(rs, cs', dynQ) = runQueryReader q (E.components es) in (singleMaybeDyn rs dynQ es, cs')
diff --git a/src/Aztecs/ECS/Query/Reader/Class.hs b/src/Aztecs/ECS/Query/Reader/Class.hs
deleted file mode 100644
--- a/src/Aztecs/ECS/Query/Reader/Class.hs
+++ /dev/null
@@ -1,26 +0,0 @@
--- |
--- 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 (QueryReaderF (..)) where
-
-import Aztecs.ECS.Component
-
--- | Query reader functor.
---
--- @since 0.10
-class (Functor f) => QueryReaderF f where
-  -- | Fetch a `Component` by its type.
-  --
-  -- @since 0.10
-  fetch :: (Component a) => f a
-
-  -- | Fetch a `Component` by its type, returning `Nothing` if it doesn't exist.
-  --
-  -- @since 0.10
-  fetchMaybe :: (Component a) => f (Maybe a)
-  fetchMaybe = Just <$> fetch
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,8 +1,11 @@
 {-# LANGUAGE BangPatterns #-}
 {-# LANGUAGE DeriveFunctor #-}
 {-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE GADTs #-}
 {-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE KindSignatures #-}
 {-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE RankNTypes #-}
 
 -- |
 -- Module      : Aztecs.ECS.System
@@ -13,151 +16,201 @@
 -- Stability   : provisional
 -- Portability : non-portable (GHC extensions)
 --
--- Systems to process queries in parallel.
+-- Systems to process entities.
 module Aztecs.ECS.System
-  ( System,
+  ( -- * Systems
+    System,
     SystemT (..),
-    MonadReaderSystem (..),
-    MonadSystem (..),
-    MonadDynamicReaderSystem (..),
-    MonadDynamicSystem (..),
+
+    -- ** Reading
+    readQuery,
+    allDyn,
+    readQueryEntities,
+
+    -- ** Writing
+    query,
+    querySingleMaybe,
+    queryDyn,
+    querySingleMaybeDyn,
+
+    -- ** Running
+    runSystemT,
+    concurrently,
+
+    -- * Internal
+    Job (..),
+    Task (..),
   )
 where
 
-import Aztecs.ECS.Component
-import Aztecs.ECS.Query
-import qualified Aztecs.ECS.Query as Q
-import Aztecs.ECS.Query.Dynamic (DynamicQueryT)
-import Aztecs.ECS.Query.Dynamic.Reader (DynamicQueryReader (..))
-import Aztecs.ECS.Query.Reader
-import Aztecs.ECS.System.Class
-import Aztecs.ECS.System.Dynamic.Class
-import Aztecs.ECS.System.Dynamic.Reader.Class
-import Aztecs.ECS.System.Reader.Class
+import Aztecs.ECS.Entity (EntityID)
+import Aztecs.ECS.Query (QueryT (..))
+import Aztecs.ECS.Query.Dynamic (DynamicQueryT, queryFilter, readDynQuery, readDynQueryEntities)
 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.Entities (Entities (..))
-import Control.Category
-import Control.Concurrent.STM
+import Control.Concurrent (forkIO)
+import Control.Concurrent.MVar
 import Control.Monad.Identity
-import Control.Monad.Reader
-import qualified Data.Foldable as F
+import Control.Monad.Trans
+import Data.Kind
 import qualified Data.Map as Map
-import Data.Set (Set)
-import Prelude hiding (all, filter, id, map, (.))
 
--- | @since 0.9
-type System = SystemT STM
+-- | System task.
+--
+-- @since 0.11
+newtype Task t (m :: Type -> Type) a
+  = Task {runTask :: ((Entities -> Entities) -> t m Entities) -> t m a}
+  deriving (Functor)
 
--- | System to process queries in parallel.
+-- | Job to be interpreted.
 --
--- @since 0.9
-newtype SystemT m a = SystemT
-  { -- | Run a system on a collection of `Entities`.
-    --
-    -- @since 0.9
-    runSystemT :: ReaderT (TVar Entities) m a
-  }
-  deriving (Functor, Applicative, Monad)
+-- @since 0.11
+data Job t m a where
+  Pure :: a -> Job t m a
+  Map :: (a -> b) -> Job t m a -> Job t m b
+  Ap :: Job t m (a -> b) -> Job t m a -> Job t m b
+  Bind :: Job t m a -> (a -> Job t m b) -> Job t m b
+  Once :: Task t m a -> Job t m a
 
--- | @since 0.9
-instance MonadDynamicSystem (DynamicQueryT STM) System where
-  mapDyn cIds q = SystemT $ do
-    wVar <- ask
-    w <- lift $ readTVar wVar
-    let !v = V.view cIds $ archetypes w
-    (o, v') <- lift $ V.mapDyn q v
-    lift . modifyTVar wVar $ V.unview v'
-    return o
-  mapSingleMaybeDyn 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 q v
-        lift . modifyTVar wVar $ V.unview v'
-        return o
-      Nothing -> return Nothing
-  filterMapDyn cIds f q = SystemT $ do
-    wVar <- ask
-    w <- lift $ readTVar wVar
-    let !v = V.filterView cIds f $ archetypes w
-    (o, v') <- lift $ V.mapDyn q v
-    lift . modifyTVar wVar $ V.unview v'
-    return o
+type System = SystemT Identity
 
--- | @since 0.9
-instance MonadSystem (QueryT STM) System where
-  map q = SystemT $ do
-    (rws, dynQ) <- runSystemT $ fromQuery q
-    runSystemT $ mapDyn (Q.reads rws <> Q.writes rws) dynQ
-  mapSingleMaybe q = SystemT $ do
-    (rws, dynQ) <- runSystemT $ fromQuery q
-    runSystemT $ mapSingleMaybeDyn (Q.reads rws <> Q.writes rws) dynQ
-  filterMap 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 (Q.reads rws <> Q.writes rws) f' dynQ
+-- | System to process entities.
+--
+-- @since 0.11
+newtype SystemT m a
+  = System {unSystem :: forall t. (MonadTrans t, Monad (t m)) => Job t m a}
 
--- | @since 0.9
-instance MonadReaderSystem QueryReader System where
-  all q = SystemT $ do
-    (cIds, dynQ) <- runSystemT $ fromQueryReader q
-    runSystemT $ allDyn cIds dynQ
-  filter 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 cIds dynQ f'
+-- | @since 0.11
+instance Functor (SystemT m) where
+  fmap f (System s) = System $ Map f s
 
--- | @since 0.9
-instance MonadDynamicReaderSystem DynamicQueryReader System where
-  allDyn cIds q = SystemT $ do
-    wVar <- ask
-    w <- lift $ readTVar wVar
-    let !v = V.view cIds $ archetypes w
-    return $
-      if V.null v
-        then runDynQueryReader q A.empty {A.entities = Map.keysSet $ entities w}
-        else V.allDyn q v
-  filterDyn cIds q f = SystemT $ do
-    wVar <- ask
-    w <- lift $ readTVar wVar
-    let !v = V.filterView cIds f $ archetypes w
-    return $ V.allDyn q v
+-- | @since 0.11
+instance (Monad m) => Applicative (SystemT m) where
+  pure a = System $ Pure a
+  (System f) <*> (System g) = System $ Ap f g
 
--- | Convert a `QueryReaderT` to a `System`.
+-- | @since 0.11
+instance (Monad m) => Monad (SystemT m) where
+  (System a) >>= f = System $ Bind a (unSystem . f)
+
+-- | @since 0.11
+instance (MonadIO m) => MonadIO (SystemT m) where
+  liftIO m = System $ Once . Task . const . lift $ liftIO m
+
+-- | Map all entities with a `DynamicQueryT`.
 --
--- @since 0.9
-fromQueryReader :: QueryReader a -> System (Set ComponentID, DynamicQueryReader a)
-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
+-- @since 0.11
+queryDyn :: (Monad m) => DynamicQueryT m a -> SystemT m [a]
+queryDyn q = System $ Once . Task $ \f -> do
+  w <- f id
+  let qf = queryFilter q
+      !v = V.view qf $ archetypes w
+  (o, v') <- lift $ V.mapDyn q v
+  _ <- f $ V.unview v'
+  return o
 
+-- | Map a single entity with a `DynamicQueryT`.
+--
+-- @since 0.11
+querySingleMaybeDyn :: (Monad m) => DynamicQueryT m a -> SystemT m (Maybe a)
+querySingleMaybeDyn q = System $ Once . Task $ \f -> do
+  w <- f id
+  let qf = queryFilter q
+  case V.viewSingle qf $ archetypes w of
+    Just v -> do
+      (o, v') <- lift $ V.mapSingleDyn q v
+      _ <- f $ V.unview v'
+      return o
+    Nothing -> return Nothing
+
+-- | Match and update all entities with a `QueryT`.
+--
+-- @since 0.11
+query :: (Monad m) => QueryT m a -> SystemT m [a]
+query q = do
+  dynQ <- fromQuery q
+  queryDyn dynQ
+
+-- | Match and update a single entity with a `QueryT`, or @Nothing@.
+--
+-- @since 0.11
+querySingleMaybe :: (Monad m) => QueryT m a -> SystemT m (Maybe a)
+querySingleMaybe q = do
+  dynQ <- fromQuery q
+  querySingleMaybeDyn dynQ
+
+-- | Match all entities with a `QueryT`.
+--
+-- @since 0.11
+readQuery :: (Monad m) => QueryT m a -> SystemT m [a]
+readQuery q = do
+  dynQ <- fromQuery q
+  allDyn dynQ
+
+-- | Match entities with a `QueryT`.
+--
+-- @since 0.11
+readQueryEntities :: (Monad m) => [EntityID] -> QueryT m a -> SystemT m [a]
+readQueryEntities es q = fromQuery q >>= queryEntitiesDyn es
+
+allDyn :: (Monad m) => DynamicQueryT m a -> SystemT m [a]
+allDyn q = System $ Once . Task $ \f -> do
+  w <- f id
+  let qf = queryFilter q
+      !v = V.view qf $ archetypes w
+  lift $
+    if V.null v
+      then readDynQuery q $ A.empty {A.entities = Map.keysSet $ entities w}
+      else V.allDyn q v
+
+queryEntitiesDyn :: (Monad m) => [EntityID] -> DynamicQueryT m a -> SystemT m [a]
+queryEntitiesDyn es q = System $ Once . Task $ \f -> do
+  w <- f id
+  let qf = queryFilter q
+      !v = V.view qf $ archetypes w
+  lift $
+    if V.null v
+      then readDynQueryEntities es q $ A.empty {A.entities = Map.keysSet $ entities w}
+      else V.allDyn q v
+
 -- | Convert a `QueryT` to a `System`.
 --
--- @since 0.9
-fromQuery :: QueryT STM a -> System (ReadsWrites, DynamicQueryT STM a)
-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
+-- @since 0.11
+fromQuery :: (Monad m) => QueryT m a -> SystemT m (DynamicQueryT m a)
+fromQuery q = System $ Once . Task $ \f -> do
+  w <- f id
+  let (cs', dynQ) = runQuery q $ components w
+  _ <- f $ const w {components = cs'}
+  return dynQ
+
+runSystemT :: (MonadTrans t, Monad (t m), Monad m) => SystemT m a -> ((Entities -> Entities) -> t m Entities) -> t m a
+runSystemT (System s) = runJob s
+
+runJob :: (MonadTrans t, Monad (t m), Monad m) => Job t m a -> ((Entities -> Entities) -> t m Entities) -> t m a
+runJob (Pure a) _ = return a
+runJob (Map f' s') f = f' <$> runJob s' f
+runJob (Ap f' a) f = runJob f' f <*> runJob a f
+runJob (Bind a f') f = runJob a f >>= \a' -> runJob (f' a') f
+runJob (Once (Task t)) f = t f
+
+concurrently :: SystemT IO a -> ((Entities -> Entities) -> IO Entities) -> IO a
+concurrently (System s) f = runJobConcurrently s $ lift . f
+
+runJobConcurrently :: Job IdentityT IO a -> ((Entities -> Entities) -> IdentityT IO Entities) -> IO a
+runJobConcurrently (Pure a) _ = return a
+runJobConcurrently (Map f' s') f = f' <$> runJobConcurrently s' f
+runJobConcurrently (Ap f' a) f = do
+  aVar <- newEmptyMVar
+  fVar <- newEmptyMVar
+  _ <- forkIO $ do
+    f'' <- runJobConcurrently f' f
+    putMVar fVar f''
+  _ <- forkIO $ do
+    a' <- runJobConcurrently a f
+    putMVar aVar a'
+  a' <- takeMVar aVar
+  f'' <- takeMVar fVar
+  return $ f'' a'
+runJobConcurrently (Bind a f') f = runJobConcurrently a f >>= \a' -> runJobConcurrently (f' a') f
+runJobConcurrently (Once (Task t)) f = runIdentityT $ t f
diff --git a/src/Aztecs/ECS/System/Class.hs b/src/Aztecs/ECS/System/Class.hs
deleted file mode 100644
--- a/src/Aztecs/ECS/System/Class.hs
+++ /dev/null
@@ -1,44 +0,0 @@
-{-# LANGUAGE FunctionalDependencies #-}
-
--- |
--- 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 GHC.Stack
-import Prelude hiding (map)
-
--- | Monadic system.
---
--- @since 0.9
-class (Monad m) => MonadSystem q m | m -> q where
-  -- | Map all matching entities with a query.
-  --
-  -- @since 0.9
-  map :: q a -> m [a]
-
-  -- | Map a single matching entity with a query, or @Nothing@.
-  --
-  -- @since 0.9
-  mapSingleMaybe :: q a -> m (Maybe a)
-
-  -- | Map a single matching entity with a query.
-  --
-  -- @since 0.9
-  mapSingle :: (HasCallStack) => q a -> m a
-  mapSingle q = do
-    res <- mapSingleMaybe q
-    case res of
-      Just a -> return a
-      Nothing -> error "Expected a single matching entity."
-
-  -- | Map all matching entities with a query and filter.
-  --
-  -- @since 0.9
-  filterMap :: q a -> QueryFilter -> m [a]
diff --git a/src/Aztecs/ECS/System/Dynamic/Class.hs b/src/Aztecs/ECS/System/Dynamic/Class.hs
deleted file mode 100644
--- a/src/Aztecs/ECS/System/Dynamic/Class.hs
+++ /dev/null
@@ -1,43 +0,0 @@
-{-# LANGUAGE FunctionalDependencies #-}
-
--- |
--- 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
-
--- | Monadic dynamic system.
---
--- @since 0.9
-class (Monad m) => MonadDynamicSystem q m | m -> q where
-  -- | Map all matching entities with a query.
-  --
-  -- @since 0.9
-  mapDyn :: Set ComponentID -> q a -> m [a]
-
-  -- | Map a single matching entity with a query, or @Nothing@.
-  --
-  -- @since 0.9
-  mapSingleMaybeDyn :: Set ComponentID -> q a -> m (Maybe a)
-
-  -- | Map a single matching entity with a query.
-  mapSingleDyn :: (HasCallStack) => Set ComponentID -> q a -> m a
-  mapSingleDyn cIds q = do
-    res <- mapSingleMaybeDyn cIds q
-    case res of
-      Just a -> return a
-      Nothing -> error "Expected a single matching entity."
-
-  -- | Map all matching entities with a query and filter.
-  --
-  -- @since 0.9
-  filterMapDyn :: Set ComponentID -> (Node -> Bool) -> q a -> m [a]
diff --git a/src/Aztecs/ECS/System/Dynamic/Reader/Class.hs b/src/Aztecs/ECS/System/Dynamic/Reader/Class.hs
deleted file mode 100644
--- a/src/Aztecs/ECS/System/Dynamic/Reader/Class.hs
+++ /dev/null
@@ -1,50 +0,0 @@
-{-# LANGUAGE FunctionalDependencies #-}
-
--- |
--- 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
-import Aztecs.ECS.World.Archetypes (Node)
-import Data.Set (Set)
-import GHC.Stack
-
--- | Monadic dynamic reader system.
---
--- @since 0.9
-class (Monad m) => MonadDynamicReaderSystem q m | m -> q where
-  -- | Match all entities with a query.
-  --
-  -- @since 0.9
-  allDyn :: Set ComponentID -> q a -> m [a]
-
-  -- | Match a single entity with a query.
-  --
-  -- @since 0.9
-  singleDyn :: (HasCallStack) => Set ComponentID -> q a -> m a
-  singleDyn cIds q = do
-    os <- allDyn cIds q
-    case os of
-      [o] -> return o
-      _ -> error "singleDyn: expected a single result, but got multiple"
-
-  -- | Match a single entity with a query, or Nothing.
-  --
-  -- @since 0.9
-  singleMaybeDyn :: Set ComponentID -> q a -> m (Maybe a)
-  singleMaybeDyn cIds q = do
-    os <- allDyn cIds q
-    return $ case os of
-      [o] -> Just o
-      _ -> Nothing
-
-  -- | Match all entities with a query and filter.
-  --
-  -- @since 0.9
-  filterDyn :: Set ComponentID -> q a -> (Node -> Bool) -> m [a]
diff --git a/src/Aztecs/ECS/System/Reader/Class.hs b/src/Aztecs/ECS/System/Reader/Class.hs
deleted file mode 100644
--- a/src/Aztecs/ECS/System/Reader/Class.hs
+++ /dev/null
@@ -1,49 +0,0 @@
-{-# LANGUAGE FunctionalDependencies #-}
-
--- |
--- 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 GHC.Stack
-import Prelude hiding (all)
-
--- | Monadic reader system.
---
--- @since 0.9
-class (Monad m) => MonadReaderSystem q m | m -> q where
-  -- | Match all entities with a query.
-  --
-  -- @since 0.9
-  all :: q a -> m [a]
-
-  -- | Match a single entity with a query.
-  --
-  -- @since 0.9
-  single :: (HasCallStack) => q a -> m a
-  single q = do
-    os <- all q
-    case os of
-      [o] -> return o
-      _ -> error "single: expected a single result"
-
-  -- | Match a single entity with a query, or @Nothing@.
-  --
-  -- @since 0.9
-  singleMaybe :: q a -> m (Maybe a)
-  singleMaybe q = do
-    os <- all q
-    return $ case os of
-      [o] -> Just o
-      _ -> Nothing
-
-  -- | Match all entities with a query and filter.
-  --
-  -- @since 0.9
-  filter :: q a -> QueryFilter -> m [a]
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
@@ -15,7 +15,6 @@
   ( View (..),
     view,
     viewSingle,
-    filterView,
     null,
     unview,
     allDyn,
@@ -25,17 +24,14 @@
   )
 where
 
-import Aztecs.ECS.Query.Dynamic (DynamicQueryT (..))
-import Aztecs.ECS.Query.Dynamic.Reader (DynamicQueryReader (..))
+import Aztecs.ECS.Query.Dynamic (DynamicQueryT (..), QueryFilter (filterWith, filterWithout), readDynQuery, runDynQuery)
 import Aztecs.ECS.World.Archetypes
 import qualified Aztecs.ECS.World.Archetypes as AS
-import Aztecs.ECS.World.Components
 import Aztecs.ECS.World.Entities (Entities)
 import qualified Aztecs.ECS.World.Entities as E
 import Data.Foldable (foldl', foldlM)
 import Data.Map.Strict (Map)
 import qualified Data.Map.Strict as Map
-import Data.Set (Set)
 import Prelude hiding (null)
 
 -- | View into a `World`, containing a subset of archetypes.
@@ -52,27 +48,17 @@
 -- | View into all archetypes containing the provided component IDs.
 --
 -- @since 0.9
-view :: Set ComponentID -> Archetypes -> View
-view cIds as = View $ AS.find cIds as
+view :: QueryFilter -> Archetypes -> View
+view qf as = View $ AS.find (filterWith qf) (filterWithout qf) as
 
 -- | View into a single archetype containing the provided component IDs.
 --
 -- @since 0.9
-viewSingle :: Set ComponentID -> Archetypes -> Maybe View
-viewSingle cIds as = case Map.toList $ AS.find cIds as of
+viewSingle :: QueryFilter -> Archetypes -> Maybe View
+viewSingle qf as = case Map.toList $ AS.find (filterWith qf) (filterWithout qf) 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 0.9
-filterView ::
-  Set ComponentID ->
-  (Node -> Bool) ->
-  Archetypes ->
-  View
-filterView cIds f as = View $ Map.filter f (AS.find cIds as)
-
 -- | @True@ if the `View` is empty.
 --
 -- @since 0.9
@@ -95,12 +81,12 @@
 -- | Query all matching entities in a `View`.
 --
 -- @since 0.9
-allDyn :: DynamicQueryReader a -> View -> [a]
+allDyn :: (Monad m) => DynamicQueryT m a -> View -> m [a]
 allDyn q v =
-  foldl'
-    ( \acc n ->
-        let as = runDynQueryReader q $ nodeArchetype n
-         in as ++ acc
+  foldlM
+    ( \acc n -> do
+        as <- readDynQuery q $ nodeArchetype n
+        return $ as ++ acc
     )
     []
     (viewArchetypes v)
@@ -108,10 +94,12 @@
 -- | Query all matching entities in a `View`.
 --
 -- @since 0.9
-singleDyn :: DynamicQueryReader a -> View -> Maybe a
-singleDyn q v = case allDyn q v of
-  [a] -> Just a
-  _ -> Nothing
+singleDyn :: (Monad m) => DynamicQueryT m a -> View -> m (Maybe a)
+singleDyn q v = do
+  as <- allDyn q v
+  return $ case as of
+    [a] -> Just a
+    _ -> Nothing
 
 -- | Map all matching entities in a `View`.
 --
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
@@ -35,8 +35,9 @@
     insertComponent,
     insertComponents,
     insertAscList,
+    map,
+    mapM,
     zipWith,
-    zipWith_,
     zipWithM,
   )
 where
@@ -48,7 +49,8 @@
 import qualified Aztecs.ECS.World.Storage.Dynamic as S
 import Control.DeepSeq
 import Control.Monad.Writer
-  ( MonadWriter (..),
+  ( MonadTrans (..),
+    MonadWriter (..),
     WriterT (..),
     runWriter,
   )
@@ -62,7 +64,7 @@
 import Data.Set (Set)
 import qualified Data.Set as Set
 import GHC.Generics
-import Prelude hiding (map, zipWith)
+import Prelude hiding (map, mapM, zipWith)
 
 -- | Archetype of entities and components.
 -- An archetype is guranteed to contain one of each stored component per entity.
@@ -154,29 +156,82 @@
 member :: ComponentID -> Archetype -> Bool
 member cId = IntMap.member (unComponentId cId) . storages
 
--- | Zip a list of components with a function and a component storage.
+-- | Map a list of components with a function and a component storage.
 --
+-- @since 0.11
+{-# INLINE map #-}
+map ::
+  forall a. (Component a) => (a -> a) -> ComponentID -> Archetype -> ([a], Archetype)
+map f cId arch =
+  let go maybeDyn = case maybeDyn of
+        Just dyn -> case fromDynamic $ storageDyn dyn of
+          Just s -> do
+            let !(as, s') = S.map @a @(StorageT a) f s
+            tell as
+            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'})
+
+-- | Map a list of components with a monadic function.
+--
+-- @since 0.11
+{-# INLINE mapM #-}
+mapM ::
+  forall m a.
+  (Monad m, Component a) =>
+  (a -> m a) ->
+  ComponentID ->
+  Archetype ->
+  m ([a], Archetype)
+mapM f cId arch = do
+  let go maybeDyn = case maybeDyn of
+        Just dyn -> case fromDynamic $ storageDyn dyn of
+          Just s -> do
+            (as, s') <- lift $ S.mapM @a @(StorageT a) f s
+            tell as
+            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.
+--
 -- @since 0.9
 {-# INLINE zipWith #-}
 zipWith ::
-  forall a c. (Component c) => [a] -> (a -> c -> c) -> ComponentID -> Archetype -> ([c], Archetype)
+  forall a b c.
+  (Component c) =>
+  [a] ->
+  (a -> c -> (b, c)) ->
+  ComponentID ->
+  Archetype ->
+  ([(b, 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'
+            let !(acs, s') = S.zipWith @c @(StorageT c) f as s
+            tell acs
             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.
+-- | Zip a list of components with a monadic function .
 --
 -- @since 0.9
 zipWithM ::
-  forall m a c. (Applicative m, Component c) => [a] -> (a -> c -> m c) -> ComponentID -> Archetype -> m ([c], Archetype)
+  forall m a b c.
+  (Applicative m, Component c) =>
+  [a] ->
+  (a -> c -> m (b, c)) ->
+  ComponentID ->
+  Archetype ->
+  m ([(b, c)], Archetype)
 zipWithM as f cId arch = do
   let go maybeDyn = case maybeDyn of
         Just dyn -> case fromDynamic $ storageDyn dyn of
@@ -189,21 +244,6 @@
         Nothing -> pure Nothing
   res <- runWriterT $ IntMap.alterF go (unComponentId cId) $ storages arch
   return (snd res, arch {storages = fst res})
-
--- | Zip a list of components with a function and a component storage.
---
--- @since 0.9
-{-# INLINE zipWith_ #-}
-zipWith_ ::
-  forall a c. (Component c) => [a] -> (a -> c -> c) -> ComponentID -> Archetype -> Archetype
-zipWith_ as f cId arch =
-  let maybeStorage = case IntMap.lookup (unComponentId cId) $ storages arch 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 -> Nothing
-        Nothing -> Nothing
-   in (empty {storages = maybe IntMap.empty (IntMap.singleton (unComponentId cId)) maybeStorage})
 
 -- | Insert a list of components into the archetype, sorted in ascending order by their `EntityID`.
 --
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
@@ -29,7 +29,6 @@
     findArchetypeIds,
     lookup,
     find,
-    map,
     adjustArchetype,
     insert,
     remove,
@@ -137,29 +136,21 @@
 
 -- | Find `ArchetypeID`s containing a set of `ComponentID`s.
 --
--- @since 0.9
-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
+-- @since 0.11
+findArchetypeIds :: Set ComponentID -> Set ComponentID -> Archetypes -> Set ArchetypeID
+findArchetypeIds w wo arches =
+  let !withIds = mapMaybe (\cId -> Map.lookup cId (componentIds arches)) (Set.elems w)
+      !withoutIds = mapMaybe (\cId -> Map.lookup cId (componentIds arches)) (Set.elems wo)
+      !withoutSet = foldl' Set.union Set.empty withoutIds
+   in case withIds of
+        (aId : aIds') -> foldl' Set.intersection aId aIds' `Set.difference` withoutSet
+        [] -> Set.empty
 
 -- | Lookup `Archetype`s containing a set of `ComponentID`s.
 --
 -- @since 0.9
-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 0.9
-map :: Set ComponentID -> (Archetype -> (a, Archetype)) -> Archetypes -> ([a], Archetypes)
-map cIds f arches =
-  let go (acc, archAcc) aId =
-        let !node = nodes archAcc Map.! aId
-            !(a, arch') = f (nodeArchetype node)
-            nodes' = Map.insert aId (node {nodeArchetype = arch'}) (nodes archAcc)
-         in (a : acc, archAcc {nodes = nodes'})
-   in foldl' go ([], arches) $ findArchetypeIds cIds arches
+find :: Set ComponentID -> Set ComponentID -> Archetypes -> Map ArchetypeID Node
+find w wo arches = Map.fromSet (\aId -> nodes arches Map.! aId) (findArchetypeIds w wo arches)
 
 -- | Lookup an `ArchetypeID` by its set of `ComponentID`s.
 --
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
@@ -1,6 +1,4 @@
 {-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE FlexibleContexts #-}
-{-# LANGUAGE InstanceSigs #-}
 {-# LANGUAGE RankNTypes #-}
 {-# LANGUAGE ScopedTypeVariables #-}
 {-# LANGUAGE TupleSections #-}
@@ -17,8 +15,8 @@
 -- Portability : non-portable (GHC extensions)
 module Aztecs.ECS.World.Bundle
   ( Bundle (..),
-    MonoidBundle (..),
-    MonoidDynamicBundle (..),
+    bundle,
+    fromDynBundle,
     runBundle,
   )
 where
@@ -26,7 +24,6 @@
 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
 import qualified Aztecs.ECS.World.Components as CS
@@ -54,15 +51,13 @@
         (cIds2, cs'', d2) = b2 cs'
      in (cIds1 <> cIds2, cs'', d1 <> d2)
 
--- | @since 0.9
-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 0.11
+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 0.9
-instance MonoidDynamicBundle Bundle where
-  dynBundle cId c = Bundle (Set.singleton cId,,dynBundle cId c)
+fromDynBundle :: DynamicBundle -> Bundle
+fromDynBundle d = Bundle (Set.empty,,d)
 
 -- | Insert a bundle of components into an archetype.
 --
diff --git a/src/Aztecs/ECS/World/Bundle/Class.hs b/src/Aztecs/ECS/World/Bundle/Class.hs
deleted file mode 100644
--- a/src/Aztecs/ECS/World/Bundle/Class.hs
+++ /dev/null
@@ -1,23 +0,0 @@
-{-# 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
-
--- | Monoid bundle of components.
---
--- @since 0.9
-class (Monoid a) => MonoidBundle a where
-  -- | Add a component to the bundle.
-  --
-  -- @since 0.9
-  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
@@ -6,11 +6,11 @@
 -- Maintainer  : matt@hunzinger.me
 -- Stability   : provisional
 -- Portability : non-portable (GHC extensions)
-module Aztecs.ECS.World.Bundle.Dynamic (DynamicBundle (..), MonoidDynamicBundle (..)) where
+module Aztecs.ECS.World.Bundle.Dynamic (DynamicBundle (..), dynBundle) where
 
+import Aztecs.ECS.Component
 import Aztecs.ECS.Entity
 import Aztecs.ECS.World.Archetype
-import Aztecs.ECS.World.Bundle.Dynamic.Class
 
 -- | Dynamic bundle of components.
 --
@@ -25,6 +25,6 @@
 instance Monoid DynamicBundle where
   mempty = DynamicBundle $ \_ arch -> arch
 
--- | @since 0.9
-instance MonoidDynamicBundle DynamicBundle where
-  dynBundle cId a = DynamicBundle $ \eId arch -> insertComponent eId cId a arch
+-- | @since 0.11
+dynBundle :: (Component a) => ComponentID -> a -> DynamicBundle
+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
deleted file mode 100644
--- a/src/Aztecs/ECS/World/Bundle/Dynamic/Class.hs
+++ /dev/null
@@ -1,20 +0,0 @@
--- |
--- 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
-
--- | Monoid bundle of dynamic components.
---
--- @since 0.9
-class MonoidDynamicBundle a where
-  -- | Add a component to the bundle by its `ComponentID`.
-  --
-  -- @since 0.9
-  dynBundle :: (Component c) => ComponentID -> c -> a
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
@@ -13,6 +13,7 @@
 
 import Control.DeepSeq
 import qualified Control.Monad
+import qualified Control.Monad as M
 import Data.Data
 import Prelude hiding (zipWith)
 import qualified Prelude
@@ -39,26 +40,25 @@
   -- | Map a function over all components in the storage.
   --
   --
-  -- @since 0.9
-  map :: (a -> a) -> s -> s
+  -- @since 0.11
+  map :: (a -> a) -> s -> ([a], s)
 
-  -- | Map a function with some input over all components in the storage.
+  -- | Map a monadic function over all components in the storage.
   --
-  -- @since 0.9
-  zipWith :: (i -> a -> a) -> [i] -> s -> ([a], s)
+  -- @since 0.11
+  mapM :: (Monad m) => (a -> m a) -> s -> m ([a], s)
 
-  -- | Map an applicative functor with some input over all components in the storage.
+  -- | Map a function with some input over all components in the storage.
   --
-  -- @since 0.9
-  zipWithM :: (Applicative m) => (i -> a -> m a) -> [i] -> s -> m ([a], s)
+  -- @since 0.11
+  zipWith :: (b -> a -> (c, a)) -> [b] -> s -> ([(c, a)], s)
 
-  -- | Map a function with some input over all components in the storage.
+  -- | Map an applicative functor with some input over all components in the storage.
   --
-  -- @since 0.9
-  zipWith_ :: (i -> a -> a) -> [i] -> s -> s
-  zipWith_ f is as = snd $ zipWith f is as
+  -- @since 0.11
+  zipWithM :: (Applicative m) => (b -> a -> m (c, a)) -> [b] -> s -> m ([(c, a)], s)
 
--- | @since 0.9
+-- | @since 0.11
 instance (Typeable a, NFData a) => Storage a [a] where
   {-# INLINE singleton #-}
   singleton a = [a]
@@ -67,10 +67,10 @@
   {-# INLINE fromAscList #-}
   fromAscList = id
   {-# INLINE map #-}
-  map = fmap
+  map f as = let as' = fmap f as in (as', as')
+  {-# INLINE mapM #-}
+  mapM f as = (\as' -> (as', as')) <$> M.mapM f as
   {-# INLINE zipWith #-}
-  zipWith f is as = let as' = Prelude.zipWith f is as in (as', as')
-  {-# INLINE zipWith_ #-}
-  zipWith_ = Prelude.zipWith
+  zipWith f is as = let as' = Prelude.zipWith f is as in (as', fmap snd as')
   {-# INLINE zipWithM #-}
-  zipWithM f is as = (\as' -> (as', as')) <$> Control.Monad.zipWithM f is as
+  zipWithM f is as = (\as' -> (as', fmap snd as')) <$> Control.Monad.zipWithM f is as
diff --git a/src/Aztecs/Hierarchy.hs b/src/Aztecs/Hierarchy.hs
deleted file mode 100644
--- a/src/Aztecs/Hierarchy.hs
+++ /dev/null
@@ -1,255 +0,0 @@
-{-# LANGUAGE ApplicativeDo #-}
-{-# LANGUAGE DeriveFunctor #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE FlexibleContexts #-}
-{-# 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 (..),
-    update,
-    Hierarchy (..),
-    toList,
-    foldWithKey,
-    mapWithKey,
-    mapWithAccum,
-    hierarchy,
-    hierarchies,
-    ParentState (..),
-    ChildState (..),
-  )
-where
-
-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 Control.DeepSeq
-import Control.Monad
-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
-
--- | Parent component.
---
--- @since 0.9
-newtype Parent = Parent
-  { -- | Parent entity ID.
-    --
-    -- @since 0.9
-    unParent :: EntityID
-  }
-  deriving (Eq, Ord, Show, Generic, NFData)
-
--- | @since 0.9
-instance Component Parent
-
--- | Parent internal state component.
---
--- @since 0.9
-newtype ParentState = ParentState {unParentState :: EntityID}
-  deriving (Show, Generic, NFData)
-
--- | @since 0.9
-instance Component ParentState
-
--- | Children component.
---
--- @since 0.9
-newtype Children = Children {unChildren :: Set EntityID}
-  deriving (Eq, Ord, Show, Semigroup, Monoid, Generic, NFData)
-
--- | @since 0.9
-instance Component Children
-
--- | Child internal state component.
-newtype ChildState = ChildState {unChildState :: Set EntityID}
-  deriving (Show, Generic, NFData)
-
--- | @since 0.9
-instance Component ChildState
-
--- | Update the parent-child relationships.
---
--- @since 0.9
-update ::
-  ( Applicative qr,
-    QueryReaderF qr,
-    DynamicQueryReaderF qr,
-    MonadReaderSystem qr s,
-    MonadAccess b m
-  ) =>
-  s (m ())
-update = do
-  parents <- S.all $ do
-    entity <- Q.entity
-    parent <- Q.fetch
-    maybeParentState <- Q.fetchMaybe @_ @ParentState
-    return (entity, unParent parent, maybeParentState)
-
-  children <- S.all $ do
-    entity <- Q.entity
-    cs <- Q.fetch
-    maybeChildState <- Q.fetchMaybe @_ @ChildState
-    return (entity, unChildren cs, maybeChildState)
-
-  let go = do
-        mapM_
-          ( \(entity, parent, maybeParentState) -> case maybeParentState of
-              Just (ParentState parentState) -> do
-                when (parent /= parentState) $ do
-                  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 . 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 . 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 . bundle . Children $ Set.insert entity parentChildren
-          )
-          parents
-        mapM_
-          ( \(entity, children', maybeChildState) -> case maybeChildState of
-              Just (ChildState childState) -> do
-                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 . bundle $ ChildState children'
-                mapM_ (\e -> A.insert e . bundle . Parent $ entity) children'
-          )
-          children
-  return go
-
--- | Hierarchy of entities.
---
--- @since 0.9
-data Hierarchy a = Node
-  { -- | Entity ID.
-    --
-    -- @since 0.9
-    nodeEntityId :: EntityID,
-    -- | Entity components.
-    nodeEntity :: a,
-    -- | Child nodes.
-    --
-    -- @since 0.9
-    nodeChildren :: [Hierarchy a]
-  }
-  deriving (Functor)
-
--- | @since 0.9
-instance Foldable Hierarchy where
-  foldMap f n = f (nodeEntity n) <> foldMap (foldMap f) (nodeChildren n)
-
--- | @since 0.9
-instance Traversable Hierarchy where
-  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 0.9
-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 0.9
-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 0.9
-mapWithKey :: (EntityID -> a -> b) -> Hierarchy a -> Hierarchy b
-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 0.9
-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 0.9
-hierarchy ::
-  (Applicative q, QueryReaderF q, DynamicQueryReaderF q, MonadReaderSystem q s) =>
-  EntityID ->
-  q a ->
-  s (Maybe (Hierarchy a))
-hierarchy e q = do
-  children <- S.all $ do
-    entity <- Q.entity
-    cs <- Q.fetch
-    a <- q
-    return (entity, (unChildren cs, a))
-
-  let childMap = Map.fromList children
-  return $ hierarchy' e childMap
-
--- | Build all hierarchies of parents to children, joined with the given query.
---
--- @since 0.9
-hierarchies ::
-  (Applicative q, QueryReaderF q, DynamicQueryReaderF q, MonadReaderSystem q s) =>
-  q a ->
-  s [Hierarchy a]
-hierarchies q = do
-  children <-
-    S.all
-      ( do
-          entity <- Q.entity
-          cs <- Q.fetch
-          a <- q
-          return (entity, (unChildren cs, a))
-      )
-
-  let childMap = Map.fromList children
-  roots <- S.filter Q.entity $ with @Children <> without @Parent
-  return $ mapMaybe (`hierarchy'` childMap) roots
-
--- | Build a hierarchy of parents to children.
---
--- @since 0.9
-hierarchy' :: EntityID -> Map EntityID (Set EntityID, a) -> Maybe (Hierarchy a)
-hierarchy' e childMap = case Map.lookup e childMap of
-  Just (cs, a) ->
-    let bs = mapMaybe (`hierarchy'` childMap) (Set.toList cs)
-     in Just
-          Node
-            { nodeEntityId = e,
-              nodeEntity = a,
-              nodeChildren = bs
-            }
-  Nothing -> Nothing
diff --git a/src/Aztecs/Input.hs b/src/Aztecs/Input.hs
deleted file mode 100644
--- a/src/Aztecs/Input.hs
+++ /dev/null
@@ -1,258 +0,0 @@
-{-# 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 (..),
-    KeyboardInput (..),
-    keyboardInput,
-    isKeyPressed,
-    wasKeyPressed,
-    wasKeyReleased,
-    handleKeyboardEvent,
-    MouseButton (..),
-    MouseInput (..),
-    mouseInput,
-    handleMouseMotion,
-  )
-where
-
-import Aztecs.ECS
-import Control.DeepSeq (NFData)
-import Data.Map.Strict (Map)
-import qualified Data.Map.Strict as Map
-import Data.Set (Set)
-import qualified Data.Set as Set
-import GHC.Generics (Generic)
-import Linear (V2 (..))
-import Linear.Affine (Point (..))
-
--- | Keyboard key.
---
--- @since 0.9
-data Key
-  = KeyA
-  | KeyB
-  | KeyC
-  | KeyD
-  | KeyE
-  | KeyF
-  | KeyG
-  | KeyH
-  | KeyI
-  | KeyJ
-  | KeyK
-  | KeyL
-  | KeyM
-  | KeyN
-  | KeyO
-  | KeyP
-  | KeyQ
-  | KeyR
-  | KeyS
-  | KeyT
-  | KeyU
-  | KeyV
-  | KeyW
-  | KeyX
-  | KeyY
-  | KeyZ
-  | Key0
-  | Key1
-  | Key2
-  | Key3
-  | Key4
-  | Key5
-  | Key6
-  | Key7
-  | Key8
-  | Key9
-  | KeyF1
-  | KeyF2
-  | KeyF3
-  | KeyF4
-  | KeyF5
-  | KeyF6
-  | KeyF7
-  | KeyF8
-  | KeyF9
-  | KeyF10
-  | KeyF11
-  | KeyF12
-  | KeyEscape
-  | KeyEnter
-  | KeySpace
-  | KeyBackspace
-  | KeyTab
-  | KeyCapsLock
-  | KeyShift
-  | KeyCtrl
-  | KeyAlt
-  | KeyLeft
-  | KeyRight
-  | KeyUp
-  | KeyDown
-  | KeyHome
-  | KeyEnd
-  | KeyPageUp
-  | KeyPageDown
-  | KeyInsert
-  | KeyDelete
-  | KeyMinus
-  | KeyEquals
-  | KeyBracketLeft
-  | KeyBracketRight
-  | KeyBackslash
-  | KeySemicolon
-  | KeyComma
-  | KeyPeriod
-  | KeySlash
-  | KeyNumLock
-  | KeyNumpad0
-  | KeyNumpad1
-  | KeyNumpad2
-  | KeyNumpad3
-  | KeyNumpad4
-  | KeyNumpad5
-  | KeyNumpad6
-  | KeyNumpad7
-  | KeyNumpad8
-  | KeyNumpad9
-  | KeyNumpadDivide
-  | KeyNumpadMultiply
-  | KeyNumpadMinus
-  | KeyNumpadPlus
-  | KeyNumpadEnter
-  | KeyNumpadPeriod
-  | KeySuper
-  | KeyMenu
-  deriving (Show, Eq, Ord, Enum, Bounded, Generic, NFData)
-
--- | Keyboard input component.
---
--- @since 0.9
-data KeyboardInput = KeyboardInput
-  { -- | Keyboard events that occured this frame.
-    --
-    -- @since 0.9
-    keyboardEvents :: !(Map Key InputMotion),
-    -- | Keys that are currently pressed.
-    --
-    -- @since 0.9
-    keyboardPressed :: !(Set Key)
-  }
-  deriving (Show, Generic, NFData)
-
--- | @since 0.9
-instance Component KeyboardInput
-
--- | Empty keyboard input.
---
--- @since 0.9
-keyboardInput :: KeyboardInput
-keyboardInput = KeyboardInput Map.empty Set.empty
-
--- | Input motion kind.
---
--- @since 0.9
-data InputMotion = Pressed | Released
-  deriving (Show, Eq, Generic, NFData)
-
--- | @True@ if this key is currently pressed.
---
--- @since 0.9
-isKeyPressed :: Key -> KeyboardInput -> Bool
-isKeyPressed key kb = Set.member key $ keyboardPressed kb
-
--- | Check for a key event that occured this frame.
---
--- @since 0.9
-keyEvent :: Key -> KeyboardInput -> Maybe InputMotion
-keyEvent key kb = Map.lookup key $ keyboardEvents kb
-
--- | @True@ if this key was pressed this frame.
---
--- @since 0.9
-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 0.9
-wasKeyReleased :: Key -> KeyboardInput -> Bool
-wasKeyReleased key kb = case keyEvent key kb of
-  Just Released -> True
-  _ -> False
-
--- | Handle a keyboard event.
---
--- @since 0.9
-handleKeyboardEvent :: Key -> InputMotion -> KeyboardInput -> KeyboardInput
-handleKeyboardEvent key motion kb =
-  KeyboardInput
-    { keyboardEvents = Map.insert key motion $ keyboardEvents kb,
-      keyboardPressed = case motion of
-        Pressed -> Set.insert key $ keyboardPressed kb
-        Released -> Set.delete key $ keyboardPressed kb
-    }
-
--- | Mouse button kind.
---
--- @since 0.9
-data MouseButton
-  = ButtonLeft
-  | ButtonMiddle
-  | ButtonRight
-  | ButtonX1
-  | ButtonX2
-  | -- | An unknown mouse button.
-    ButtonExtra !Int
-  deriving (Eq, Ord, Show, Generic, NFData)
-
--- | Mouse input component.
---
--- @since 0.9
-data MouseInput = MouseInput
-  { -- | Mouse position in screen-space.
-    --
-    -- @since 0.9
-    mousePosition :: !(Point V2 Int),
-    -- | Mouse offset since last frame.
-    --
-    -- @since 0.9
-    mouseOffset :: !(V2 Int),
-    -- | Mouse button states.
-    --
-    -- @since 0.9
-    mouseButtons :: !(Map MouseButton InputMotion)
-  }
-  deriving (Show, Generic, NFData)
-
--- | @since 0.9
-instance Component MouseInput
-
--- | Empty mouse input.
---
--- @since 0.9
-mouseInput :: MouseInput
-mouseInput = MouseInput (P 0) (V2 0 0) Map.empty
-
--- | Handle a mouse motion event.
---
--- @since 0.9
-handleMouseMotion :: V2 Int -> MouseInput -> MouseInput
-handleMouseMotion delta mouse =
-  mouse
-    { mouseOffset = delta,
-      mousePosition = mousePosition mouse + P delta
-    }
diff --git a/src/Aztecs/Time.hs b/src/Aztecs/Time.hs
deleted file mode 100644
--- a/src/Aztecs/Time.hs
+++ /dev/null
@@ -1,30 +0,0 @@
-{-# 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
-import GHC.Generics
-
--- | Time component.
---
--- @since 0.9
-newtype Time = Time
-  { -- | Elapsed time (in milliseconds).
-    --
-    -- @since 0.9
-    elapsedMS :: Word32
-  }
-  deriving (Eq, Ord, Num, Show, Generic, NFData)
-
-instance Component Time
diff --git a/src/Aztecs/Transform.hs b/src/Aztecs/Transform.hs
deleted file mode 100644
--- a/src/Aztecs/Transform.hs
+++ /dev/null
@@ -1,169 +0,0 @@
-{-# LANGUAGE AllowAmbiguousTypes #-}
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE FlexibleContexts #-}
-{-# LANGUAGE FlexibleInstances #-}
-{-# LANGUAGE RankNTypes #-}
-{-# 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 (..),
-
-    -- ** 2D
-    Transform2D,
-    transform2d,
-
-    -- * Size
-    Size (..),
-
-    -- ** 2D
-    Size2D,
-    size2D,
-
-    -- * Systems
-    update,
-    update2d,
-    propagate,
-    propagate2d,
-  )
-where
-
-import Aztecs.ECS
-import qualified Aztecs.ECS.Access as A
-import qualified Aztecs.ECS.Query as Q
-import Aztecs.Hierarchy (Hierarchy, hierarchies, mapWithAccum, toList)
-import Control.DeepSeq
-import GHC.Generics
-import Linear
-
--- | Transform component.
---
--- @since 0.9
-data Transform v r = Transform
-  { -- | Translation.
-    --
-    -- @since 0.9
-    transformTranslation :: !v,
-    -- | Rotation.
-    --
-    -- @since 0.9
-    transformRotation :: !r,
-    -- | Scale.
-    --
-    -- @since 0.9
-    transformScale :: !v
-  }
-  deriving (Eq, Show, Generic, NFData)
-
--- | @since 0.9
-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 0.9
-instance (Num v, Num r) => Monoid (Transform v r) where
-  mempty = Transform 0 0 0
-
--- | 2D transform component.
---
--- @since 0.9
-type Transform2D = Transform (V2 Int) Int
-
--- | Empty transform.
---
--- @since 0.9
-transform2d :: Transform2D
-transform2d = Transform (V2 0 0) 0 (V2 1 1)
-
--- | @since 0.9
-instance Component (Transform (V2 Int) Int)
-
--- | Size component.
---
--- @since 0.9
-newtype Size v = Size {unSize :: v}
-  deriving (Generic, NFData)
-
--- | @since 0.9
-type Size2D = Size (V2 Int)
-
--- | Empty size.
---
--- @since 0.9
-size2D :: Size (V2 Integer)
-size2D = Size (V2 0 0)
-
--- | @since 0.9
-instance Component (Size (V2 Int))
-
--- | Propagate a hierarchy of transform components.
---
--- @since 0.9
-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 0.9
-update ::
-  forall q s b m a.
-  ( Applicative q,
-    QueryReaderF q,
-    DynamicQueryReaderF q,
-    MonadReaderSystem q s,
-    Component a,
-    Monoid a,
-    MonadAccess b m
-  ) =>
-  s (m ())
-update = mapM_ (\(e, a) -> A.insert e $ bundle a) . concatMap toList <$> propagate @_ @_ @a
-
--- | Propagate and update all hierarchies of transform components.
---
--- @since 0.9
-update2d ::
-  ( Applicative q,
-    QueryReaderF q,
-    DynamicQueryReaderF q,
-    MonadReaderSystem q s,
-    MonadAccess b m
-  ) =>
-  s (m ())
-update2d = update @_ @_ @_ @_ @Transform2D
-
--- | Propagate all hierarchies of transform components.
---
--- @since 0.9
-propagate ::
-  ( Applicative q,
-    QueryReaderF q,
-    DynamicQueryReaderF q,
-    MonadReaderSystem q s,
-    Component a,
-    Monoid a
-  ) =>
-  s [Hierarchy a]
-propagate = do
-  hs <- hierarchies Q.fetch
-  return $ map propagateHierarchy hs
-
--- | Propagate all hierarchies of `Transform2D` components.
---
--- @since 0.9
-propagate2d ::
-  ( Applicative q,
-    QueryReaderF q,
-    DynamicQueryReaderF q,
-    MonadReaderSystem q s
-  ) =>
-  s [Hierarchy Transform2D]
-propagate2d = propagate
diff --git a/src/Aztecs/Window.hs b/src/Aztecs/Window.hs
deleted file mode 100644
--- a/src/Aztecs/Window.hs
+++ /dev/null
@@ -1,30 +0,0 @@
-{-# 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
-import GHC.Generics
-
--- | Window component.
---
--- @since 0.9
-newtype Window = Window
-  { -- | Window title.
-    --
-    -- @since 0.9
-    windowTitle :: String
-  }
-  deriving (Show, Generic, NFData)
-
--- | @since 0.9
-instance Component Window
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -9,9 +9,10 @@
 
 module Main (main) where
 
-import Aztecs
+import Aztecs.ECS
 import Aztecs.ECS.Component (ComponentID (ComponentID))
 import qualified Aztecs.ECS.Query as Q
+import qualified Aztecs.ECS.Query.Dynamic as Q
 import qualified Aztecs.ECS.World as W
 import Control.DeepSeq
 import Data.Functor.Identity (Identity (runIdentity))
@@ -34,11 +35,11 @@
 
 main :: IO ()
 main = hspec $ do
-  describe "Aztecs.ECS.Query.single" $ do
+  describe "Aztecs.ECS.Query.querySingle" $ do
     it "queries a single entity" prop_querySingle
-  describe "Aztecs.ECS.Query.mapSingle" $ do
+  describe "Aztecs.ECS.Query.queryMapSingle" $ do
     it "maps a single entity" $ property prop_queryMapSingle
-  describe "Aztecs.ECS.Query.all" $ do
+  describe "Aztecs.ECS.Query.query" $ do
     it "queries an empty world" prop_queryEmpty
     it "queries dynamic components" $ property prop_queryDyn
     it "queries a typed component" $ property prop_queryTypedComponent
@@ -55,49 +56,49 @@
 
 prop_queryEmpty :: Expectation
 prop_queryEmpty =
-  let res = fst $ Q.all (Q.fetch @_ @X) $ W.entities W.empty in res `shouldMatchList` []
+  let res = fst . runIdentity . Q.query (fetch @_ @X) $ W.entities W.empty in res `shouldMatchList` []
 
 -- | Query all components from a list of `ComponentID`s.
 queryComponentIds ::
-  forall q a.
-  (Applicative q, DynamicQueryReaderF q, Component a) =>
+  forall f a.
+  (Monad f, Component a) =>
   [ComponentID] ->
-  q (EntityID, [a])
+  QueryT f (EntityID, [a])
 queryComponentIds =
   let go cId qAcc = do
-        x' <- Q.fetchDyn @_ @a cId
+        x' <- Q.fromDyn $ Q.fetchDyn @a cId
         (e, xs) <- qAcc
         return (e, x' : xs)
    in foldr go ((,) <$> Q.entity <*> pure [])
 
 prop_queryDyn :: [[X]] -> Expectation
 prop_queryDyn xs =
-  let spawn xs' (acc, wAcc) =
-        let spawn' x (bAcc, cAcc, idAcc) =
-              ( dynBundle (ComponentID idAcc) x <> bAcc,
+  let s xs' (acc, wAcc) =
+        let s' x (bAcc, cAcc, idAcc) =
+              ( fromDynBundle (dynBundle (ComponentID idAcc) x) <> bAcc,
                 (x, ComponentID idAcc) : cAcc,
                 idAcc + 1
               )
-            (b, cs, _) = foldr spawn' (mempty, [], 0) xs'
+            (b, cs, _) = foldr s' (mempty, [], 0) xs'
             (e, wAcc') = W.spawn b wAcc
          in ((e, cs) : acc, wAcc')
-      (es, w) = foldr spawn ([], W.empty) xs
+      (es, w) = foldr s ([], W.empty) xs
       go (e, cs) = do
         let q = queryComponentIds $ map snd cs
-            (res, _) = Q.all q $ W.entities w
+            (res, _) = runIdentity . Q.query q $ W.entities w
         return $ res `shouldContain` [(e, map fst cs)]
    in mapM_ go es
 
 prop_queryTypedComponent :: [X] -> Expectation
 prop_queryTypedComponent xs = do
   let w = foldr (\x -> snd . W.spawn (bundle x)) W.empty xs
-      (res, _) = Q.all Q.fetch $ W.entities w
+      (res, _) = runIdentity . Q.query Q.fetch $ W.entities w
   res `shouldMatchList` xs
 
 prop_queryTwoTypedComponents :: [(X, Y)] -> Expectation
 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
+      (res, _) = runIdentity . Q.query ((,) <$> Q.fetch <*> Q.fetch) $ W.entities w
   res `shouldMatchList` xys
 
 prop_queryThreeTypedComponents :: [(X, Y, Z)] -> Expectation
@@ -108,21 +109,21 @@
         y <- Q.fetch
         z <- Q.fetch
         pure (x, y, z)
-      (res, _) = Q.all q $ W.entities w
+      (res, _) = runIdentity . Q.query 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
+      (res, _) = runIdentity . Q.readSingle 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.adjust (\_ (X x) -> X $ x + 1) (pure ())
-      w' = foldr (\_ es -> snd . runIdentity $ Q.mapSingle q es) (W.entities w) [1 .. n]
-      (res, _) = Q.single Q.fetch w'
+      q = Q.zipFetchMap (\_ (X x) -> X $ x + 1) (pure ())
+      w' = foldr (\_ es -> snd . runIdentity $ Q.querySingle q es) (W.entities w) [1 .. n]
+      (res, _) = runIdentity $ Q.readSingle Q.fetch w'
    in res `shouldBe` X (fromIntegral n)
 
 {-TODO
