diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,3 @@
+# Changelog for circuit-breaker
+
+## Unreleased changes
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Author name here (c) 2019
+
+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 Author name here 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,52 @@
+# circuit-breaker
+Circuit breakers are an error handling machine inspired by the circuit breakers used in electrical systems. Just
+like their namesake, software circuit breakers prevent pushing traffic through a failing component
+until it has recovered.
+
+As of the current version, all `CircuitBreaker`s use a "leaky bucket" approach to backoffs.
+In the definition of a circuit breaker, the first `Natural` argument is the number of milliseconds that need to pass before an error is expunged.
+Coupled with an error threshold argument, this provides a surprising amount of flexibility for cirucit breakers in the wild, although you'll want to monitor and tune them over time.
+
+### Using a circuit breaker
+The following short example illustrates how to define a circuit breaker & use it.
+
+```haskell
+testBreaker :: CircuitBreaker "Test" 1000 4
+testBreaker = undefined
+
+main :: IO ()
+main = do
+    -- Initializes the empty storage for all circuit breakers
+    cbConf <- initialBreakerState
+    -- Perform a bunch of "work". Because we have a 50% failure rate and trigger the breaker after four
+    -- errors, this will cause the breaker to disable additional calls.
+    forM_ [1..30] $ const . noteError . flip runReaderT cbConf $ withBreaker testBreaker computation
+
+    -- simulating a backoff long enough to decrement the accumulated errors.
+    threadDelay 5000000
+
+    noteError . flip runReaderT cbConf $ withBreaker testBreaker computation
+    where
+        noteError action = print =<< catchAny action (const $ pure (Left Failed))
+
+
+
+-- | This computation fails 50% of the time
+computation :: ReaderT CircuitBreakerConf IO Int
+computation = do
+    shouldFail <- liftIO randomIO
+    when shouldFail $ error "Failed"
+    pure 42
+```
+
+The first thing to notice is that the `CircuitBreaker` definition exists entirely at the type level.
+This guarantees that the definition of a particular `CircuitBreaker` can't somehow change at runtime.
+
+All calls to `withBreaker` require a `CircuitBreakerConf` to be present in a reader environment.
+`initialBreakerState` simply initializes an empty `CircuitBreakerConf` to save you the trouble of creating one yourself.
+
+### Contributing
+PRs and issues are welcome! Please let me know what you think could be improved or submit the patch yourself.
+
+### License
+MIT
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/app/Main.hs b/app/Main.hs
new file mode 100644
--- /dev/null
+++ b/app/Main.hs
@@ -0,0 +1,35 @@
+module Main where
+
+import System.CircuitBreaker (CircuitBreaker, withBreaker, initialBreakerState,
+    CircuitBreakerConf, CircutBreakerError(..))
+
+import Control.Monad(forM_)
+import Control.Monad.Trans (liftIO)
+import Control.Monad.Reader (ReaderT, runReaderT)
+import Control.Concurrent(threadDelay)
+import Control.Monad (when, forever)
+import UnliftIO.Exception (catchAny)
+import System.Random (randomIO)
+
+testBreaker :: CircuitBreaker "Test" 1000 4
+testBreaker = undefined
+
+main :: IO ()
+main = do
+    cbConf <- initialBreakerState
+    -- TODO the api for calling withBreaker kinda sucks. It'd be great to drop the 'undefined'
+
+    forM_ [1..30] $ const . noteError . flip runReaderT cbConf $ withBreaker testBreaker computation
+    threadDelay 5000000
+    noteError . flip runReaderT cbConf $ withBreaker testBreaker computation
+    where
+        noteError action = print =<< catchAny action (const $ pure (Left Failed))
+
+
+
+-- | This computation fails 50% of the time
+computation :: ReaderT CircuitBreakerConf IO Int
+computation = do
+    shouldFail <- liftIO randomIO
+    when shouldFail $ error "Failed"
+    pure 42
diff --git a/circuit-breaker.cabal b/circuit-breaker.cabal
new file mode 100644
--- /dev/null
+++ b/circuit-breaker.cabal
@@ -0,0 +1,98 @@
+cabal-version: 1.12
+
+-- This file has been generated from package.yaml by hpack version 0.31.1.
+--
+-- see: https://github.com/sol/hpack
+--
+-- hash: b70b09864192ada221e36052b2e656f25c7d31214c5eaacd042f183f5efb13b1
+
+name:           circuit-breaker
+version:        0.1.0.0
+synopsis:       An implementation of the "circuit breaker" pattern to disable repeated calls to a failing system
+description:    Please see the README on GitHub at <https://github.com/ChrisCoffey/circuit-breaker#readme>
+category:       System
+homepage:       https://github.com/ChrisCoffey/circuit-breaker#readme
+bug-reports:    https://github.com/ChrisCoffey/circuit-breaker/issues
+author:         Chris Coffey
+maintainer:     chris@foldl.io
+copyright:      2019 Chris Coffey
+license:        BSD3
+license-file:   LICENSE
+build-type:     Simple
+extra-source-files:
+    README.md
+    ChangeLog.md
+
+source-repository head
+  type: git
+  location: https://github.com/ChrisCoffey/circuit-breaker
+
+library
+  exposed-modules:
+      Lib
+      System.CircuitBreaker
+      System.CircuitBreaker.Management
+  other-modules:
+      Paths_circuit_breaker
+  hs-source-dirs:
+      src
+  default-extensions: GADTs FlexibleInstances DataKinds MultiParamTypeClasses FunctionalDependencies TypeApplications KindSignatures ScopedTypeVariables TypeFamilies ExistentialQuantification OverloadedStrings
+  build-depends:
+      base >=4.7 && <5
+    , mtl
+    , random
+    , text
+    , time
+    , transformers
+    , unliftio
+    , unliftio-core
+    , unordered-containers
+  default-language: Haskell2010
+
+executable circuit-breaker-exe
+  main-is: Main.hs
+  other-modules:
+      Paths_circuit_breaker
+  hs-source-dirs:
+      app
+  default-extensions: GADTs FlexibleInstances DataKinds MultiParamTypeClasses FunctionalDependencies TypeApplications KindSignatures ScopedTypeVariables TypeFamilies ExistentialQuantification OverloadedStrings
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      base >=4.7 && <5
+    , circuit-breaker
+    , mtl
+    , random
+    , text
+    , time
+    , transformers
+    , unliftio
+    , unliftio-core
+    , unordered-containers
+  default-language: Haskell2010
+
+test-suite circuit-breaker-test
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  other-modules:
+      Paths_circuit_breaker
+  hs-source-dirs:
+      test
+  default-extensions: GADTs FlexibleInstances DataKinds MultiParamTypeClasses FunctionalDependencies TypeApplications KindSignatures ScopedTypeVariables TypeFamilies ExistentialQuantification OverloadedStrings
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      QuickCheck
+    , base >=4.7 && <5
+    , circuit-breaker
+    , mtl
+    , quickcheck-instances
+    , random
+    , tasty
+    , tasty-hunit
+    , tasty-quickcheck
+    , text
+    , time
+    , transformers
+    , unliftio
+    , unliftio-core
+    , unordered-containers
+  default-language: Haskell2010
diff --git a/src/Lib.hs b/src/Lib.hs
new file mode 100644
--- /dev/null
+++ b/src/Lib.hs
@@ -0,0 +1,8 @@
+module Lib
+    ( someFunc
+    ) where
+
+someFunc :: IO ()
+someFunc = putStrLn "someFunc"
+
+
diff --git a/src/System/CircuitBreaker.hs b/src/System/CircuitBreaker.hs
new file mode 100644
--- /dev/null
+++ b/src/System/CircuitBreaker.hs
@@ -0,0 +1,216 @@
+-- |
+-- Module:          System.CircuitBreaker
+-- Copyright:       (c) 2019 Chris Coffey
+-- License:         MIT
+-- Maintainer:      chris@foldl.io
+-- Stability:       experimental
+-- Portability:     portable
+--
+-- This is a batteries mostly-included circuit breaker library. It provides automatic backoff behavior by
+-- preventing calls to a failing resource. For example, if a particular service is currently overloaded and
+-- begins failing to respond to requests, then the circuit breaker will detect those errors and prevent
+-- further calls from actually hitting the server in question. Once the service has had a chance to stabilize
+-- the circuit breaker will allow a "testing" call to pass through. If it succeeds, then all other calls are
+-- allowed to flow through. If it fails then it waits a bit longer before testing again.
+
+module System.CircuitBreaker (
+    CircuitBreaker,
+    CircuitBreakerConf(..),
+    withBreaker,
+
+    CircutBreakerError(..),
+    HasCircuitConf(..),
+    CircuitState(..),
+    CircuitAction(..),
+    ErrorThreshold(..),
+    CBCondition(..),
+
+    initialBreakerState,
+    -- | Exported for testing
+    breakerTransitionGuard,
+    breakerTryPerformAction,
+    decrementErrorCount
+) where
+
+import Control.Monad (forever, void)
+import Control.Monad.Trans (liftIO)
+import Control.Monad.IO.Unlift (MonadUnliftIO)
+import Control.Monad.Reader (MonadReader, asks, ReaderT, ask)
+import Data.Maybe (fromMaybe, isNothing)
+import Numeric.Natural (Natural)
+import GHC.TypeLits (Symbol, KnownSymbol, symbolVal, Nat, KnownNat, natVal)
+import Data.Proxy (Proxy(..))
+import UnliftIO.Concurrent (forkIO, threadDelay)
+import UnliftIO.Exception (bracketOnError, catchDeep)
+import UnliftIO.MVar (MVar, putMVar, takeMVar, newMVar, swapMVar)
+import qualified Data.Text as T
+import qualified Data.HashMap.Strict as M
+
+newtype CircuitBreakerConf = CBConf {
+    cbBreakers :: MVar (M.HashMap T.Text (MVar CircuitState))
+    }
+
+-- | Initialize the storage for the process' circuit breakers
+initialBreakerState :: MonadUnliftIO m => m CircuitBreakerConf
+initialBreakerState = do
+    mv <- newMVar M.empty
+    pure CBConf {cbBreakers = mv}
+
+-- | The current condition of a CircutBreaker may be Active, Testing, or Waiting for the
+-- timeout to elapse.
+data CBCondition
+    = Active
+    | Testing
+    | Waiting
+    deriving (Show, Eq, Ord, Bounded, Enum)
+
+data CircuitState = CircuitState {
+      errorCount :: Natural
+    , currentState :: !CBCondition
+    } deriving (Show)
+
+-- | A constraint that allows pullng the circuit breaker
+class HasCircuitConf env where
+    getCircuitState :: env -> CircuitBreakerConf
+
+instance HasCircuitConf CircuitBreakerConf where
+    getCircuitState = id
+
+newtype ErrorThreshold = ET Natural
+newtype DripFreq = DF Natural
+
+-- | Access a Circut Breaker's label at runtime
+reifyCircuitBreaker :: forall label df et. (KnownSymbol label, KnownNat df, KnownNat et) =>
+    CircuitBreaker label df et
+    -> (T.Text, DripFreq, ErrorThreshold)
+reifyCircuitBreaker _ = (l, df, et)
+    where
+        l = T.pack $ symbolVal (Proxy :: Proxy label)
+        df = DF . (* 1000) . fromIntegral $ natVal (Proxy :: Proxy df)
+        et = ET . fromIntegral $ natVal (Proxy :: Proxy et)
+
+-- | The definition of a particular circuit breaker
+data CircuitBreaker (label :: Symbol) (dripFreq :: Nat) (errorThreshold :: Nat)
+
+data CircutBreakerError
+    = Failed
+    | CircuitBreakerClosed T.Text
+    deriving (Show, Eq, Ord)
+
+data CircuitAction
+    = SkipClosed
+    | Run
+    deriving (Eq, Show)
+
+
+-- | Brackets a computation with short-circuit logic. If an uncaught error occurs, then the
+-- circuit breaker opens, which causes all additional threads entering the wrapped code to fail
+-- until the specified timeout has expired. Once the timeout expires, a single thread may enter
+-- the protected region. If the call succeeds, then the circuit breaker allows all other traffic
+-- through. Otherwise, it resets the timeout, after which the above algorithm repeats.
+--
+-- Important Note: This does not catch errors. If an IO error is thrown, it will bubble up from
+-- this function. Internally, if the breaker is tripped, it will prevent further calls
+withBreaker :: (KnownSymbol label, KnownNat df, KnownNat et, Monad m,
+    MonadUnliftIO m, MonadReader env m, HasCircuitConf env) =>
+    CircuitBreaker label df et
+    -> m a
+    -> m (Either CircutBreakerError a)
+withBreaker breakerDefinition action = do
+    -- Get the current circut breaker
+    breakerCell <- cbBreakers <$> asks getCircuitState
+    breakers <- takeMVar breakerCell
+    let mbs = label `M.lookup` breakers
+        newBreaker = CircuitState {
+            errorCount = 0,
+            currentState = Active
+            }
+    bs <- maybe (newMVar newBreaker) pure mbs
+
+    -- If it happens to be the first time this block has been called, store the new breakers state.
+    -- Also initialize a monitor thread to drip errors
+    if isNothing mbs
+        then do
+            putMVar breakerCell $ M.insert label bs breakers
+            monitor bs
+        else  putMVar breakerCell breakers
+    -- bracketOnError the action
+    -- Read the current status & determine whether to perform the action
+    --      0) Get the current time
+    --      1) Try to read the MVar
+    --      2) If it is Nothing, then there is a test underway & we're still closed
+    --      3) If it is a Just, check if we're 'Waiting' & the timeout has not elapsed
+    --          4) If the timeout has elapsed, enter 'Testing' state
+    --          5) Otherwise, run the computation
+    --      6) If the computation fails, set the error time and enter 'Waiting' state
+    bracketOnError (breakerTransitionGuard bs (ET et)) (onError bs) (breakerTryPerformAction label action bs)
+    where
+        (label, DF dripFreq, ET et) = reifyCircuitBreaker breakerDefinition
+
+        -- In the event of an uncaught error during the bracketed computation, flip the circuit breaker to 'Waiting'
+        onError bs Run = do
+            bs' <- takeMVar bs
+            let ec' = 1 + errorCount bs'
+                state = if ec' >= et then Waiting else Active
+            putMVar bs $ CircuitState {errorCount = ec', currentState = state}
+
+        -- Run a
+        monitor bs =
+            void . forkIO . forever $ do
+                threadDelay $ fromIntegral dripFreq
+                decrementErrorCount bs
+
+-- | Given the current state of the circuit breaker, determine what action should
+-- be taken during the body of the 'bracket'.
+--
+-- Condition rules:
+-- 1) If the circuit breaker is in 'Active' then pass the call on
+-- 2) If the circuit breaker is in 'Testing' then fail the call
+-- 3) If the circuit breaker is in 'Waiting' and the timeout has not elapsed then fail the call
+-- 4) If the circuit breaker is in 'Waiting' and the timeout has elapsed then convert to 'Testing' and try the call
+breakerTransitionGuard :: (MonadUnliftIO m) =>
+    MVar CircuitState
+    -> ErrorThreshold
+    -> m CircuitAction
+breakerTransitionGuard bs (ET et) = do
+    cb <- takeMVar bs
+    let elapsed = errorCount cb < fromIntegral et
+    case currentState cb of
+        Waiting | elapsed -> do
+            putMVar bs $ cb {currentState = Testing}
+            pure Run
+        Active -> do
+            putMVar bs cb
+            pure Run
+        Waiting -> do
+            putMVar bs cb
+            pure SkipClosed
+        Testing -> do
+            putMVar bs cb
+            pure SkipClosed
+
+-- | Conditionally performs an action with in a circuit breaker, as determined by the current breaker state.
+breakerTryPerformAction :: MonadUnliftIO m =>
+    T.Text -- ^ label
+    -> m a -- ^ Action to perform
+    -> MVar CircuitState -- ^ Pointer to the breaker state
+    -> CircuitAction
+    -> m (Either CircutBreakerError a)
+breakerTryPerformAction label _ _ SkipClosed =
+    pure . Left $ CircuitBreakerClosed label
+breakerTryPerformAction label action bs Run = do
+    res <- Right <$> action
+    bs' <- takeMVar bs
+    putMVar bs (bs' {currentState = Active})
+    pure res
+
+-- | Decrements a counter
+decrementErrorCount :: MonadUnliftIO m =>
+    MVar CircuitState
+    -> m ()
+decrementErrorCount breaker = do
+    rawState <- takeMVar breaker
+    let ec = errorCount rawState
+    if ec == 0
+    then putMVar breaker rawState
+    else putMVar breaker $ rawState {errorCount = ec -1}
diff --git a/src/System/CircuitBreaker/Management.hs b/src/System/CircuitBreaker/Management.hs
new file mode 100644
--- /dev/null
+++ b/src/System/CircuitBreaker/Management.hs
@@ -0,0 +1,1 @@
+module System.CircuitBreaker.Management where
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,107 @@
+module Main (main) where
+
+import System.CircuitBreaker
+
+import Control.Concurrent.MVar
+import Data.Either
+import Numeric.Natural
+import Test.Tasty
+import Test.Tasty.HUnit
+import Test.Tasty.QuickCheck
+
+main :: IO ()
+main = defaultMain $ testGroup "System.CircuitBreaker" [
+      decrementProperties
+    , evaluationProperties
+    , transitionProperties
+    ]
+
+transitionProperties :: TestTree
+transitionProperties = testGroup "transition guard" [
+    testCase "Waiting -> SkipClosed iff errors remain" $ do
+        bs <- newMVar $ CircuitState {errorCount = 1, currentState = Waiting}
+        action <- breakerTransitionGuard bs (ET 1)
+        action @=? SkipClosed
+    , testCase "Respects the error threshold" $ do
+        bs <- newMVar $ CircuitState {errorCount = 9, currentState = Waiting}
+        action <- breakerTransitionGuard bs (ET 10)
+        bs' <- readMVar bs
+        action @=? Run
+        currentState bs' @=? Testing
+    , testCase "Testing is a serialized state" $ do
+        bs <- newMVar $ CircuitState {errorCount = 0, currentState = Testing}
+        action <- breakerTransitionGuard bs (ET 1)
+        action @=? SkipClosed
+    , testCase "Active always -> Run" $ do
+        bs <- newMVar $ CircuitState {errorCount = 5, currentState = Active}
+        action <- breakerTransitionGuard bs (ET 1)
+        action @=? Run
+    ]
+
+evaluationProperties :: TestTree
+evaluationProperties = testGroup "evaluation" [
+    testProperty "never evaluates if SkipClosed is provided" $ \(CircState rawBS, ca) ->
+        cover 35 (ca == SkipClosed) "Skip Closed" .
+        cover 35 (ca == Run) "Run" . ioProperty $ do
+            bs <- newMVar rawBS
+            cell <- newMVar False
+            res <- breakerTryPerformAction "test" (swapMVar cell True) bs ca
+            cellV <- readMVar cell
+            pure $ if isLeft res
+                   then not cellV
+                   else cellV
+    , testProperty "always sets status to active if it runs" $ \(CircState rawBS, ca) ->
+        cover 35 (ca == SkipClosed) "Skip Closed" .
+        cover 35 (ca == Run ) "Run" . ioProperty $ do
+            bs <- newMVar rawBS
+            cell <- newMVar False
+            res <- breakerTryPerformAction "test" (swapMVar cell True) bs ca
+            cellV <- readMVar cell
+            ba' <- readMVar bs
+            pure $ if cellV
+                   then currentState ba' == Active
+                   else isLeft res
+    ]
+
+decrementProperties :: TestTree
+decrementProperties = testGroup "error decrement" [
+    testProperty "decrements, but not through zero" $ \(CircState rawBS) ->
+        cover 5 (errorCount rawBS < 2) "Minimum Boundary" . ioProperty $ do
+            bs <- newMVar rawBS
+            decrementErrorCount bs
+            val <- readMVar bs
+            pure $ if errorCount rawBS == 0
+                   then errorCount rawBS == errorCount val
+                   else errorCount val == errorCount rawBS - 1
+
+    , testProperty "never changes state" $ \(CircState rawBS) -> ioProperty $ do
+        bs <- newMVar rawBS
+        decrementErrorCount bs
+        val <- readMVar bs
+        pure $ currentState val == currentState rawBS
+
+    , testProperty "Always >= 0" $ \(CircState rawBS) ->
+        cover 2 (errorCount rawBS == 0) "Minimum Boundary" . ioProperty $ do
+        bs <- newMVar rawBS
+        decrementErrorCount bs
+        val <- readMVar bs
+        pure $ errorCount val >= 0
+    ]
+
+newtype CircState = CircState CircuitState deriving Show
+instance Arbitrary CircState where
+    arbitrary = do
+        st <- elements [Active, Testing, Waiting]
+        ec <- arbitrary
+        pure . CircState $ CircuitState {
+              errorCount = ec
+            , currentState = st
+            }
+
+instance Arbitrary CircuitAction where
+    arbitrary = elements [Run, SkipClosed]
+
+
+instance Arbitrary Natural where
+    arbitrary = fromIntegral . abs <$> (arbitrary :: Gen Int)
+
