diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Revision history for lazy-bracket
+
+## 0.1.0.0 -- YYYY-mm-dd
+
+* First version. Released on an unsuspecting world.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2021, Daniel Diaz
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Daniel Diaz nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,37 @@
+# lazy-bracket
+
+Sometimes, when `bracket`ing some piece of code, the
+resource acquired by the `bracket` won't be actually used:
+
+- Finding a result in an in-memory cache can mean that a database query is
+avoided, and the database connection stays untouched. 
+
+- You might be providing some resource (again, think database connection) to every
+REST endpoint handler in your API, even if some handlers don't make use of the
+resource, because treating these handlers as special cases would be tedious.
+
+In order to be more frugal and avoid unnecessary resource acquisitions, one
+possible approach is to delay the acquisition to 
+ the first time the resource
+is actually used, if it's used at all. 
+
+What's more, certain "control" operations
+on the resource don't need to be applied immediately, and instead can wait
+until the resource is eventually acquired, or be omitted altogether if the resource isn't acquired:
+
+- It's wasteful to acquire a file handle just to perform `hSetBuffering`, if we are not going to write to the handle.
+
+- It's wasteful to acquire a database connection just to begin a transaction, if we aren't going to perform any query.
+
+This package provides lazy versions of `bracket` for which resource acquisition
+is delayed until first use, and control operations are only applied once resources are acquired.
+
+## Links
+
+- The inspiration:
+[LazyConnectionDataSourceProxy](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jdbc/datasource/LazyConnectionDataSourceProxy.html)
+form Java's Spring Framework.
+
+  > Proxy for a target DataSource, fetching actual JDBC Connections lazily, i.e. not until first creation of a Statement. Connection initialization properties like auto-commit mode, transaction isolation and read-only mode will be kept and applied to the actual JDBC Connection as soon as an actual Connection is fetched (if ever). Consequently, commit and rollback calls will be ignored if no Statements have been created.
+
+  > This DataSource proxy allows to avoid fetching JDBC Connections from a pool unless actually necessary. JDBC transaction control can happen without fetching a Connection from the pool or communicating with the database; this will be done lazily on first creation of a JDBC Statement.
diff --git a/lazy-bracket.cabal b/lazy-bracket.cabal
new file mode 100644
--- /dev/null
+++ b/lazy-bracket.cabal
@@ -0,0 +1,54 @@
+cabal-version:       3.0
+
+name:                lazy-bracket
+version:             0.1.0.0
+synopsis:            A bracket with lazy resource allocation.
+description:         A version of bracket which returns a lazily allocated
+                     resource. The allocation happens when (if) the resource 
+                     is accessed for the first time.
+
+                     Some control operations can be stashed and applied once the
+                     resource is allocated, or skipped altogether if the
+                     resource is never allocated.
+license:             BSD-3-Clause
+license-file:        LICENSE
+author:              Daniel Diaz
+maintainer:          diaz_carrete@yahoo.com
+category:            Control
+extra-source-files:  CHANGELOG.md, README.md
+
+source-repository    head
+  type:     git
+  location: https://github.com/danidiaz/lazy-bracket.git
+
+common common
+  build-depends:       base >=4.10.0.0 && < 5,
+                       exceptions ^>= 0.10,
+  default-language:    Haskell2010
+
+library
+  import: common
+  exposed-modules:     LazyBracket
+  hs-source-dirs:      lib 
+
+test-suite test
+  import: common
+  type:                exitcode-stdio-1.0
+  hs-source-dirs:      test
+  main-is:             tests.hs
+  build-depends:       
+    lazy-bracket,
+    tasty              >= 1.3.1,
+    tasty-hunit        >= 0.10.0.2,
+
+-- VERY IMPORTANT for doctests to work: https://stackoverflow.com/a/58027909/1364288
+-- http://hackage.haskell.org/package/cabal-doctest
+test-suite doctests
+  import:              common
+  ghc-options:         -threaded
+  type:                exitcode-stdio-1.0
+  hs-source-dirs:      test
+  main-is:             doctests.hs
+  build-depends:       
+                       lazy-bracket, 
+                       doctest            ^>= 0.20,
diff --git a/lib/LazyBracket.hs b/lib/LazyBracket.hs
new file mode 100644
--- /dev/null
+++ b/lib/LazyBracket.hs
@@ -0,0 +1,226 @@
+{-# LANGUAGE BlockArguments #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
+
+-- | This module provides variants of the 'bracket' function that delay the
+-- acquisition of the resource until it's used for the first time. If
+-- the resource is never used, it will never be acquired.
+--
+-- A trivial example. This bracket code with a faulty acquisition doesn't throw an exception because the
+-- resource is never accessed:
+--
+-- >>> :{
+--  lazyBracket 
+--    (throwIO (userError "oops")) 
+--    (\_ -> pure ()) 
+--    \Resource {} -> do 
+--      pure () 
+-- :}
+-- 
+-- But this code does:
+--
+-- >>> :{
+--  lazyBracket 
+--    (throwIO (userError "oops")) 
+--    (\_ -> pure ()) 
+--    \Resource {accessResource} -> do 
+--      _ <- accessResource
+--      pure () 
+-- :}
+-- *** Exception: user error (oops)
+--
+-- To be even more lazy, certain kinds of operations on the resource do not
+-- trigger acquisition: instead, they are stashed and applied once the resource
+-- has been acquired for other reasons.
+--
+-- Look at the sequence of ouput messages here:
+--
+-- >>> :{
+--  lazyBracket 
+--    (putStrLn "acquired!") 
+--    (\() -> putStrLn "released!") 
+--    \Resource {accessResource, controlResource} -> do 
+--      controlResource \() -> putStrLn "control op 1 - delayed"
+--      putStrLn "before acquiring"
+--      _ <- accessResource
+--      putStrLn "after acquiring"
+--      controlResource \() -> putStrLn "control op 2 - immediately executed"
+--      pure () 
+-- :}
+-- before acquiring
+-- acquired!
+-- control op 1 - delayed
+-- after acquiring
+-- control op 2 - immediately executed
+-- released!
+--
+-- If we never access the resource, the release function and the stashed
+-- operations are not executed:
+--
+-- >>> :{
+--  lazyBracket 
+--    (putStrLn "acquired!") 
+--    (\() -> putStrLn "released!") 
+--    \Resource {accessResource, controlResource} -> do 
+--      controlResource \() -> putStrLn "control op 1 - never happens"
+--      pure () 
+-- :}
+--
+--
+module LazyBracket
+  ( -- * Lazy brackets that delay resource acquisition.
+    lazyBracket,
+    lazyGeneralBracket,
+    lazyGeneralBracket_,
+
+    -- * Resource wrapper.
+    Resource (..),
+
+    -- * Re-exports.
+    ExitCase (..),
+  )
+where
+
+import Control.Concurrent.MVar
+import Control.Monad
+import Control.Monad.Catch
+import Control.Monad.IO.Class
+
+-- | A wrapper type over resources that delays resource acquisition.
+--
+-- Because one must be careful with the kinds of functions that are passed to 'controlResource',
+-- it might be a good idea to define convenience wrappers over 'Resource' with
+-- more restricted interfaces.
+data Resource a = Resource
+  { -- | Action to get hold of the resource. Will trigger resource acquisition
+    -- and apply all stashed control operations the first time it's run.
+    accessResource :: IO a,
+    -- | Immediately apply a \"control\" operation to the underlying resource if
+    -- the resource has already been acquired, otherwise stash the operation
+    -- with the intention of applying it once the resource is eventually acquired.
+    -- If the resource is never acquired, stashed operations are discarded.
+    --
+    -- By \"control\" operations we mean operations that are not essential in and of
+    -- themselves, only serve to modify the behaviour of operations that are actually
+    -- essential, and can be omitted if no essential operations take place.
+    --
+    -- Some examples:
+    --
+    -- For file handle resources, @hSetBuffering@ is a valid control
+    -- operation, whereas actually writing bytes to the handle is not.
+    --
+    -- For database connection resources, beginning a transaction is a valid control
+    -- operation, whereas performing an INSERT is not.
+    controlResource :: (a -> IO ()) -> IO ()
+  }
+
+-- | A version of 'Contro.Monad.Catch.bracket' for which the resource is not
+-- acquired at the beginning, but the first time it's used in the main callback.
+-- If the resource is never used, it won't be acquired.
+lazyBracket ::
+  (MonadIO m, MonadMask m) =>
+  -- | Computation to run to acquire the resource.
+  IO a ->
+  -- | Computation to run to release the resource, in case it was acquired.
+  (a -> m c) ->
+  -- | Computation to run in-between (might trigger resource acquisition).
+  (Resource a -> m b) ->
+  -- | Returns the value from the in-between computation
+  m b 
+lazyBracket acquire release action = do
+  lazyGeneralBracket_
+    acquire
+    (\a _ -> release a)
+    action
+
+data ResourceState a
+  = NotYetAcquired (a -> IO ())
+  | AlreadyAcquired a
+
+-- | A version of 'Contro.Monad.Catch.generalBracket' for which the resource is not
+-- acquired at the beginning, but the first time it's used in the main callback.
+-- If the resource is never used, it won't be acquired.
+--
+lazyGeneralBracket ::
+  forall m a b c.
+  (MonadIO m, MonadMask m) =>
+  -- | Computation to run to acquire the resource
+  IO a ->
+  -- | Computation to run to release the resource, in case it was acquired
+  --
+  -- The release function has knowledge of how the main callback was exited: by
+  -- normal completion, by a runtime exception, or otherwise aborted.
+  -- This can be useful when acquiring resources from resource pools,
+  -- to decide whether to return the resource to the pool or to destroy it.
+  (a -> ExitCase b -> m c) ->
+  -- | Computation to run in-between (might trigger resource acquisition)
+  (Resource a -> m b) ->
+  -- | Returns the value from the in-between computation, and also of the
+  -- release computation, if it took place.
+  m (b, Maybe c)
+lazyGeneralBracket acquire release action = do
+  ref <- liftIO $ newMVar @(ResourceState a) (NotYetAcquired mempty)
+  let accessResource = do
+        (resource, pendingOperations) <- do
+          modifyMVarMasked ref \case
+            NotYetAcquired pendingOperations -> do
+              resource <- acquire
+              pure (AlreadyAcquired resource, (resource, pendingOperations))
+            resourceState@(AlreadyAcquired a) -> do
+              pure (resourceState, (a, mempty))
+        pendingOperations resource -- no need to perform these inside the mask
+        pure resource
+  let controlResource operation = do
+        runNow <- do
+          modifyMVarMasked ref \case
+            NotYetAcquired pendingOperations -> do
+              pure (NotYetAcquired (pendingOperations <> operation), mempty)
+            resourceState@(AlreadyAcquired a) -> do
+              pure (resourceState, operation a)
+        runNow
+  let lazyResource = Resource {accessResource, controlResource}
+  -- We ignore the 'Resource' argument because we extract the unwrapped
+  -- version from the 'MVar'.
+  let lazyRelease (_ :: Resource a) exitCase = do
+        action <- liftIO $ do
+          -- we don't mask here, already provided by generalBracket
+          modifyMVar ref \case
+            NotYetAcquired _ -> do
+              pure (NotYetAcquired mempty, \_ -> pure Nothing)
+            AlreadyAcquired a -> do
+              pure (NotYetAcquired mempty, fmap Just <$> release a)
+        -- If we ran this inside the modifyMVar, an exception during release
+        -- would prevent resetting the state to NotYetAcquired. Do we want that?
+        action exitCase
+  generalBracket (pure lazyResource) lazyRelease action
+
+
+-- | Slightly simpler version of 'lazyGeneralBracket' that doesn't return the result of the 
+-- release computation.
+lazyGeneralBracket_ ::
+  forall m a b c.
+  (MonadIO m, MonadMask m) =>
+  -- | computation to run to acquire the resource
+  IO a ->
+  -- | computation to run to release the resource, in case it was acquired
+  (a -> ExitCase b -> m c) ->
+  -- | computation to run in-between (might trigger resource acquisition)
+  (Resource a -> m b) ->
+  -- | returns the value from the in-between computation.
+  m b
+lazyGeneralBracket_ acquire release action = do
+  (b, _) <-
+    lazyGeneralBracket
+      acquire
+      release
+      action
+  pure b
+
+-- $setup
+--
+-- >>> :set -XBlockArguments
+-- >>> :set -XNamedFieldPuns
+-- >>> import LazyBracket
+-- >>> import Control.Exception
diff --git a/test/doctests.hs b/test/doctests.hs
new file mode 100644
--- /dev/null
+++ b/test/doctests.hs
@@ -0,0 +1,7 @@
+module Main (main) where
+
+import Test.DocTest
+main = doctest [
+      "-ilib"
+    , "lib/LazyBracket.hs"
+    ]
diff --git a/test/tests.hs b/test/tests.hs
new file mode 100644
--- /dev/null
+++ b/test/tests.hs
@@ -0,0 +1,166 @@
+{-# LANGUAGE BlockArguments #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
+{-# LANGUAGE UndecidableInstances #-}
+
+module Main (main) where
+
+import Control.Exception
+import Data.IORef
+import LazyBracket
+import Test.Tasty
+import Test.Tasty.HUnit
+
+tests :: TestTree
+tests =
+  testGroup
+    "All"
+    [ testCase "doesAcquire" doesAcquire,
+      testCase "doesAcquireEx" doesAcquireEx,
+      testCase "doesNotAcquireTwice" doesNotAcquireTwice,
+      testCase "noAcquisition" noAcquisition,
+      testCase "noAcquisitionEx" noAcquisitionEx
+    ]
+
+data TestResourceState
+  = NotYetAcquired
+  | AlreadyAcquired
+  | Disposed
+  deriving (Show, Eq)
+
+data TestOps = TestOpA | TestOpB deriving (Show, Eq)
+
+doesAcquire :: Assertion
+doesAcquire = do
+  ref <- newIORef NotYetAcquired
+  opsRef <- newIORef @[TestOps] []
+  let refMustBe msg expectedState = do
+        testState <- readIORef ref
+        assertEqual msg expectedState testState
+  let opsRefMustBe msg expectedOpsState = do
+        opsState <- readIORef opsRef
+        assertEqual msg expectedOpsState opsState
+  _ <-
+    lazyBracket
+      (writeIORef ref AlreadyAcquired)
+      (\_ -> writeIORef ref Disposed)
+      ( \Resource {accessResource, controlResource} -> do
+          refMustBe "not acquired at the beginning" NotYetAcquired
+          controlResource (\_ -> modifyIORef opsRef (<> [TestOpA]))
+          refMustBe "control op doesn't trigger acquisition" NotYetAcquired
+          opsRefMustBe "control op not executed before acquisition" []
+          _ <- accessResource
+          refMustBe "access to resource triggers acquisition" AlreadyAcquired
+          opsRefMustBe "access to resource triggers pending ops" [TestOpA]
+          writeIORef opsRef []
+          _ <- accessResource
+          opsRefMustBe "re-accessing the resource doesn't re-execute delayed control ops" []
+          controlResource (\_ -> modifyIORef opsRef (<> [TestOpB]))
+          opsRefMustBe "control ops when already acquired execute immediately" [TestOpB]
+          pure ()
+      )
+  refMustBe "release function must run" Disposed
+  pure ()
+
+data DummyEx = DummyEx deriving (Show, Eq)
+instance Exception DummyEx
+
+doesAcquireEx :: Assertion
+doesAcquireEx = do
+  ref <- newIORef NotYetAcquired
+  opsRef <- newIORef @[TestOps] []
+  let refMustBe msg expectedState = do
+          testState <- readIORef ref
+          assertEqual msg expectedState testState
+  e <- try @DummyEx do
+    _ <-
+        lazyBracket
+        (writeIORef ref AlreadyAcquired)
+        (\_ -> writeIORef ref Disposed)
+        ( \Resource {accessResource, controlResource} -> do
+            _ <- accessResource
+            throwIO DummyEx
+            pure ()
+        )
+    pure ()
+  case e of 
+      Left DummyEx -> do
+        refMustBe "release function must run" Disposed
+        pure ()
+      Right _ -> assertFailure "Exception should have bubbled up"
+
+
+doesNotAcquireTwice :: Assertion
+doesNotAcquireTwice = do
+  let bombs = pure () : repeat (throwIO $ userError "boom!")
+  bombsRef <- newIORef @[IO ()] bombs
+  let attempt = do
+        action <- atomicModifyIORef bombsRef \(b : bs) -> (bs, b)
+        action
+  lazyBracket
+    attempt
+    (\_ -> pure ())
+    ( \Resource {accessResource} -> do
+        _ <- accessResource
+        _ <- accessResource
+        pure ()
+    )
+  pure ()
+
+noAcquisition :: Assertion
+noAcquisition = do
+  ref <- newIORef NotYetAcquired
+  opsRef <- newIORef @[TestOps] []
+  let refMustBe msg expectedState = do
+        testState <- readIORef ref
+        assertEqual msg expectedState testState
+  let opsRefMustBe msg expectedOpsState = do
+        opsState <- readIORef opsRef
+        assertEqual msg expectedOpsState opsState
+  _ <-
+    lazyBracket
+      (throwIO (userError "should not run"))
+      (\_ -> writeIORef ref Disposed)
+      ( \Resource {accessResource, controlResource} -> do
+          controlResource (\_ -> modifyIORef opsRef (<> [TestOpA]))
+          pure ()
+      )
+  refMustBe "release function must not run" NotYetAcquired
+  opsRefMustBe "no ops should trigger" []
+  pure ()
+
+noAcquisitionEx :: Assertion
+noAcquisitionEx = do
+  e <- try @DummyEx do
+    ref <- newIORef NotYetAcquired
+    opsRef <- newIORef @[TestOps] []
+    let refMustBe msg expectedState = do
+            testState <- readIORef ref
+            assertEqual msg expectedState testState
+    let opsRefMustBe msg expectedOpsState = do
+            opsState <- readIORef opsRef
+            assertEqual msg expectedOpsState opsState
+    _ <-
+        lazyBracket
+        (throwIO (userError "should not run"))
+        (\_ -> writeIORef ref Disposed)
+        ( \Resource {accessResource, controlResource} -> do
+            controlResource (\_ -> modifyIORef opsRef (<> [TestOpA]))
+            throwIO DummyEx
+            pure ()
+        )
+    refMustBe "release function must not run" NotYetAcquired
+    opsRefMustBe "no ops should trigger" []
+    pure ()
+  case e of 
+      Left DummyEx -> pure ()
+      Right _ -> assertFailure "Exception should have bubbled up"
+
+main :: IO ()
+main = defaultMain tests
