diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,3 @@
+0.1.0
+-----
+Initial release.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Moritz Kiefer (c) 2016
+
+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 Moritz Kiefer 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,7 @@
+# lrucaching
+
+[![Build Status](https://travis-ci.org/cocreature/lrucaching.svg?branch=master)](https://travis-ci.org/cocreature/lrucaching)
+
+An implementation of lrucaches based on a
+[blogpost](https://jaspervdj.be/posts/2015-02-24-lru-cache.html) by
+Jasper Van der Jeugt.
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/lrucaching.cabal b/lrucaching.cabal
new file mode 100644
--- /dev/null
+++ b/lrucaching.cabal
@@ -0,0 +1,50 @@
+name:                  lrucaching
+version:               0.1.0
+synopsis:              LRU cache
+description:           Please see README.md
+homepage:              https://github.com/cocreature/lrucaching#readme
+license:               BSD3
+license-file:          LICENSE
+author:                Moritz Kiefer
+maintainer:            moritz.kiefer@purelyfunctional.org
+copyright:             2016
+category:              Unknown
+build-type:            Simple
+extra-source-files:    CHANGELOG.md
+                       README.md
+cabal-version:         >=1.10
+tested-with:           GHC == 7.8.4, GHC == 7.10.3, GHC == 8.0.1
+
+library
+  hs-source-dirs:      src
+  exposed-modules:     Data.LruCache
+                       Data.LruCache.IO
+                       Data.LruCache.Internal
+  build-depends:       base     >= 4.7  && < 5
+                     , deepseq  >= 1.4  && < 1.5
+                     , hashable >= 1.2  && < 1.3
+                     , psqueues >= 0.2  && < 0.3
+                     , vector   >= 0.11 && < 0.12
+  ghc-options:         -Wall
+  default-language:    Haskell2010
+
+test-suite lru-test
+  type:                exitcode-stdio-1.0
+  hs-source-dirs:      test
+  main-is:             Spec.hs
+  other-modules:       Data.LruCacheSpec
+                       Data.LruCache.IOSpec
+                       Data.LruCache.SpecHelper
+  build-depends:       base
+                     , containers   >= 0.5 && < 0.6
+                     , deepseq
+                     , hashable
+                     , hspec        >= 2.2 && < 2.3
+                     , lrucaching
+                     , QuickCheck   >= 2.8 && < 2.10
+                     , transformers >= 0.4 && < 0.6
+  default-language:    Haskell2010
+
+source-repository head
+  type:                git
+  location:            https://github.com/cocreature/lrucaching
diff --git a/src/Data/LruCache.hs b/src/Data/LruCache.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/LruCache.hs
@@ -0,0 +1,108 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DeriveFoldable #-}
+{-# LANGUAGE DeriveFunctor #-}
+{-# LANGUAGE DeriveTraversable #-}
+{-|
+Module      : Data.LruCache
+Copyright   : (c) Moritz Kiefer, 2016
+              (c) Jasper Van der Jeugt, 2015
+License     : BSD3
+Maintainer  : moritz.kiefer@purelyfunctional.org
+Pure API to an LRU cache.
+-}
+module Data.LruCache
+  ( LruCache
+  , Priority
+  , empty
+  , insert
+  , insertView
+  , lookup
+  ) where
+
+import qualified Data.HashPSQ as HashPSQ
+import           Data.Hashable (Hashable)
+import           Data.Maybe (isNothing)
+import           Prelude hiding (lookup)
+
+import           Data.LruCache.Internal
+
+-- | Create an empty 'LruCache' of the given size.
+empty :: Int -> LruCache k v
+empty capacity
+  | capacity < 1 = error "LruCache.empty: capacity < 1"
+  | otherwise    =
+      LruCache 
+        { lruCapacity = capacity
+        , lruSize     = 0
+        , lruTick     = 0
+        , lruQueue    = HashPSQ.empty
+        }
+
+-- | Restore 'LruCache' invariants returning the evicted element if any.
+--
+-- When the logical clock reaches its maximum value and all values are
+-- evicted 'Nothing' is returned.
+trim' :: (Hashable k, Ord k) => LruCache k v -> (Maybe (k, v), LruCache k v)
+trim' c
+  | lruTick c == maxBound     = (Nothing, empty (lruCapacity c))
+  | lruSize c > lruCapacity c = 
+      let Just (k, _, v) = HashPSQ.findMin (lruQueue c)
+          c' = c  { lruSize  = lruSize c - 1
+                  , lruQueue = HashPSQ.deleteMin (lruQueue c)
+                  }
+      in seq c' (Just (k, v), c')
+  | otherwise                 = (Nothing, c)
+
+-- TODO benchmark to see if this is actually faster than snd . trim'
+-- | Restore 'LruCache' invariants. For performance reasons this is
+-- not @snd . trim'@.
+trim :: (Hashable k, Ord k) => LruCache k v -> LruCache k v
+trim c
+  | lruTick c == maxBound     = empty (lruCapacity c)
+  | lruSize c > lruCapacity c = 
+      c  { lruSize  = lruSize c - 1
+         , lruQueue = HashPSQ.deleteMin (lruQueue c)
+         }
+  | otherwise                 = c
+
+-- | Insert an element into the 'LruCache'.
+insert :: (Hashable k, Ord k) => k -> v -> LruCache k v -> LruCache k v
+insert key val c =
+  trim $!
+  let (mbOldVal,queue) = HashPSQ.insertView key (lruTick c) val (lruQueue c)
+  in c  { lruSize  = if isNothing mbOldVal
+                     then lruSize c + 1
+                     else lruSize c
+        , lruTick  = lruTick c + 1
+        , lruQueue = queue
+        }
+
+-- | Insert an element into the 'LruCache' returning the evicted
+-- element if any.
+--
+-- When the logical clock reaches its maximum value and all values are
+-- evicted 'Nothing' is returned.
+insertView :: (Hashable k, Ord k) => k -> v -> LruCache k v -> (Maybe (k, v), LruCache k v)
+insertView key val cache =
+  let (mbOldVal,queue) = 
+        HashPSQ.insertView key (lruTick cache) val (lruQueue cache)
+  in trim' $! cache 
+       { lruSize  = if isNothing mbOldVal
+                    then lruSize cache + 1
+                    else lruSize cache
+       , lruTick  = lruTick cache + 1
+       , lruQueue = queue
+       }
+
+-- | Lookup an element in an 'LruCache' and mark it as the least
+-- recently accessed.
+lookup :: (Hashable k, Ord k) => k -> LruCache k v -> Maybe (v, LruCache k v)
+lookup k c = 
+  case HashPSQ.alter lookupAndBump k (lruQueue c) of
+    (Nothing, _) -> Nothing
+    (Just x, q)  ->
+      let !c' = trim $ c {lruTick = lruTick c + 1, lruQueue = q}
+      in Just (x, c')
+  where
+    lookupAndBump Nothing       = (Nothing, Nothing)
+    lookupAndBump (Just (_, x)) = (Just x,  Just ((lruTick c), x))
diff --git a/src/Data/LruCache/IO.hs b/src/Data/LruCache/IO.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/LruCache/IO.hs
@@ -0,0 +1,70 @@
+{-|
+Module      : Data.LruCache.IO
+Copyright   : (c) Moritz Kiefer, 2016
+              (c) Jasper Van der Jeugt, 2015
+License     : BSD3
+Maintainer  : moritz.kiefer@purelyfunctional.org
+Convenience module for the common case of caching results of IO actions.
+-}
+module Data.LruCache.IO
+  ( LruHandle(..)
+  , cached
+  , newLruHandle
+  , StripedLruHandle(..)
+  , stripedCached
+  , newStripedHandle
+  ) where
+
+import           Control.Applicative ((<$>))
+import           Data.Hashable (Hashable, hash)
+import           Data.IORef (IORef, atomicModifyIORef', newIORef)
+import           Data.Vector (Vector)
+import qualified Data.Vector as Vector
+import           Prelude hiding (lookup)
+
+import           Data.LruCache
+
+-- | Store a LRU cache in an 'IORef to be able to conveniently update it.
+newtype LruHandle k v = LruHandle (IORef (LruCache k v))
+
+-- | Create a new LRU cache of the given size.
+newLruHandle :: Int -> IO (LruHandle k v)
+newLruHandle capacity = LruHandle <$> newIORef (empty capacity)
+
+-- | Return the cached result of the action or, in the case of a cache
+-- miss, execute the action and insert it in the cache.
+cached :: (Hashable k, Ord k) => LruHandle k v -> k -> IO v -> IO v
+cached (LruHandle ref) k io =
+  do lookupRes <- atomicModifyIORef' ref $ \c ->
+       case lookup k c of
+         Nothing      -> (c,  Nothing)
+         Just (v, c') -> (c', Just v)
+     case lookupRes of
+       Just v  -> return v
+       Nothing ->
+         do v <- io
+            atomicModifyIORef' ref $ \c -> (insert k v c, ())
+            return v
+
+-- | Using a stripe of multiple handles can improve the performance in
+-- the case of concurrent accesses since several handles can be
+-- accessed in parallel.
+newtype StripedLruHandle k v = StripedLruHandle (Vector (LruHandle k v))
+
+-- | Create a new 'StripedHandle' with the given number of stripes and
+-- the given capacity for each stripe.
+newStripedHandle :: Int -> Int -> IO (StripedLruHandle k v)
+newStripedHandle numStripes capacityPerStripe =
+  StripedLruHandle <$> Vector.replicateM numStripes (newLruHandle capacityPerStripe)
+
+-- | Striped version of 'cached'.
+stripedCached ::
+  (Hashable k, Ord k) =>
+  StripedLruHandle k v ->
+  k ->
+  IO v ->
+  IO v
+stripedCached (StripedLruHandle v) k =
+    cached (v Vector.! idx) k
+  where
+    idx = hash k `mod` Vector.length v
diff --git a/src/Data/LruCache/Internal.hs b/src/Data/LruCache/Internal.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/LruCache/Internal.hs
@@ -0,0 +1,42 @@
+{-# LANGUAGE DeriveFoldable #-}
+{-# LANGUAGE DeriveFunctor #-}
+{-# LANGUAGE DeriveTraversable #-}
+{-|
+Module      : Data.LruCache.Internal
+Copyright   : (c) Moritz Kiefer, 2016
+              (c) Jasper Van der Jeugt, 2015
+License     : BSD3
+Maintainer  : moritz.kiefer@purelyfunctional.org
+
+This module contains internal datastructures. 
+No guarantees are made as to the stability of this module
+and violating invariants can result in unspecified behavior.
+-}
+module Data.LruCache.Internal
+  ( LruCache(..)
+  , Priority
+  ) where
+  
+import           Control.DeepSeq (NFData,rnf)
+import           Data.Int
+import qualified Data.HashPSQ as HashPSQ
+import           Data.Foldable (Foldable)
+import           Data.Traversable (Traversable)
+
+-- | Logical time at which an element was last accessed.
+type Priority = Int64
+
+-- | LRU cache based on hashing. The times of access are stored in a
+-- monotonically increasing 'Int64', when that time is at 'maxbound'
+-- the cache is emptied.
+data LruCache k v = LruCache
+  { lruCapacity :: !Int                         -- ^ The maximum number of elements in the queue
+  , lruSize :: !Int                             -- ^ The current number of elements in the queue
+  , lruTick :: !Priority                        -- ^ The next logical time
+  , lruQueue :: !(HashPSQ.HashPSQ k Priority v) -- ^ Underlying priority queue
+  } 
+  deriving (Eq,Show,Functor,Foldable,Traversable)
+
+instance (NFData k, NFData v) => NFData (LruCache k v) where
+  rnf (LruCache cap size tick queue) =
+    rnf cap `seq` rnf size `seq` rnf tick `seq` rnf queue
diff --git a/test/Data/LruCache/IOSpec.hs b/test/Data/LruCache/IOSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Data/LruCache/IOSpec.hs
@@ -0,0 +1,47 @@
+module Data.LruCache.IOSpec
+  (spec
+  ) where
+
+import           Control.Monad (foldM_)
+import           Control.Monad.IO.Class
+import           Data.IORef
+import           Data.Set (Set)
+import qualified Data.Set as Set
+import           Test.Hspec
+import qualified Test.QuickCheck as QC
+import qualified Test.QuickCheck.Monadic as QC
+
+import           Data.LruCache.IO
+import           Data.LruCache.SpecHelper
+
+spec :: Spec
+spec =
+  do describe "cached" $ do
+       it "evicts leasts recently used elements" $ do
+         QC.property historic
+
+-- | Tests if elements not evicted have been recently accessed.
+historic ::
+  SmallInt ->             -- ^ Capacity
+  [(SmallInt, String)] -> -- ^ Key-value pairs
+  QC.Property             -- ^ Property
+historic (SmallInt capacity) pairs = QC.monadicIO $
+  do h <- liftIO $ newLruHandle capacity
+     foldM_ (step h) [] pairs
+     where
+       step h history (k, v) = do
+         wasInCacheRef <- liftIO $ newIORef True
+         _             <- liftIO $ cached h k $ 
+           do writeIORef wasInCacheRef False
+              return v
+         wasInCache    <- liftIO $ readIORef wasInCacheRef
+         let recentKeys = nMostRecentKeys capacity Set.empty history
+         QC.assert (Set.member k recentKeys == wasInCache)
+         return ((k, v) : history)
+
+nMostRecentKeys :: Ord k => Int -> Set k -> [(k, v)] -> Set k
+nMostRecentKeys _ keys []   = keys
+nMostRecentKeys n keys ((k, _) : history)
+  | Set.size keys >= n      = keys
+  | otherwise               =
+      nMostRecentKeys n (Set.insert k keys) history
diff --git a/test/Data/LruCache/SpecHelper.hs b/test/Data/LruCache/SpecHelper.hs
new file mode 100644
--- /dev/null
+++ b/test/Data/LruCache/SpecHelper.hs
@@ -0,0 +1,61 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+{-|
+Module      : Data.LruCache.SpecHelper
+Copyright   : (c) Moritz Kiefer, 2016
+              (c) Jasper Van der Jeugt, 2015
+License     : BSD3
+Maintainer  : moritz.kiefer@purelyfunctional.org
+-}
+module Data.LruCache.SpecHelper where
+
+import           Control.Applicative ((<$>),(<*>))
+import           Data.Foldable (foldl')
+import           Data.Hashable
+import           Prelude hiding (lookup)
+import qualified Test.QuickCheck as QC
+
+import           Data.LruCache
+
+data CacheAction k v
+  = InsertAction k v
+  | LookupAction k
+  deriving (Show,Eq,Ord)
+
+instance (QC.Arbitrary k, QC.Arbitrary v) => 
+          QC.Arbitrary (CacheAction k v) where
+  arbitrary = QC.oneof
+    [ InsertAction <$> QC.arbitrary <*> QC.arbitrary
+    , LookupAction <$> QC.arbitrary
+    ]
+
+applyCacheAction :: 
+  (Hashable k, Ord k) =>
+  CacheAction k v ->
+  LruCache k v ->
+  LruCache k v
+applyCacheAction (InsertAction k v) c = 
+  insert k v c
+applyCacheAction (LookupAction k)   c = 
+  case lookup k c of
+    Nothing      -> c
+    Just (_, c') -> c'
+
+instance forall k v. 
+         (QC.Arbitrary k, QC.Arbitrary v, Hashable k, Ord k) =>
+         QC.Arbitrary (LruCache k v) where
+  arbitrary = do
+    capacity <- QC.choose (1, 50)
+    (actions :: [CacheAction k v])  <- QC.arbitrary
+    let !cache = empty capacity
+    return $! foldl' (\c a -> applyCacheAction a c) cache actions
+
+newtype SmallInt = SmallInt Int
+  deriving (Eq, Ord, Show)
+
+instance QC.Arbitrary SmallInt where
+  arbitrary = SmallInt <$> QC.choose (1, 100)
+
+instance Hashable SmallInt where
+  hashWithSalt salt (SmallInt x) = (salt + x) `mod` 10
diff --git a/test/Data/LruCacheSpec.hs b/test/Data/LruCacheSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Data/LruCacheSpec.hs
@@ -0,0 +1,51 @@
+module Data.LruCacheSpec 
+  (spec
+  ) where
+
+import Data.Hashable
+import Data.Maybe
+import Prelude hiding (lookup)
+import Test.Hspec
+import Test.QuickCheck
+
+import Data.LruCache
+import Data.LruCache.SpecHelper
+
+spec :: Spec
+spec = 
+  do describe "insertView" $ do
+       it "evicts elements that were previously there" $ do 
+         property (evictExisted :: LruCache SmallInt Int -> SmallInt -> Int -> Bool)
+       it "removes evicted elements" $ do
+         property (evictRemoved :: LruCache SmallInt Int -> SmallInt -> Int -> Bool)
+     describe "insert" $ do
+       it "inserts elements" $ do
+         property (insertExists :: LruCache SmallInt Int -> SmallInt -> Int -> Property)
+
+
+insertExists :: 
+  (Hashable k, Ord k, Eq v, Show v) =>
+  LruCache k v ->
+  k ->
+  v ->
+  Property
+insertExists cache k v =
+  let cache' = insert k v cache
+  in fmap fst (lookup k cache') === Just v
+
+evictExisted :: (Hashable k, Ord k, Eq v) => LruCache k v -> k -> v -> Bool
+evictExisted cache k v =
+  let evicted = fst (insertView k v cache)
+  in case evicted of
+       Nothing       -> True
+       Just (k', v') -> 
+         case lookup k' cache of
+           Nothing       -> False
+           Just (v'', _) -> v' == v''
+
+evictRemoved :: (Hashable k, Ord k) => LruCache k v -> k -> v -> Bool
+evictRemoved cache k v =
+  let (evicted, cache') = insertView k v cache
+  in case evicted of
+       Nothing     -> True
+       Just (k',_) -> isNothing (lookup k' cache') 
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,1 @@
+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
