diff --git a/Data/HashRing.hs b/Data/HashRing.hs
new file mode 100644
--- /dev/null
+++ b/Data/HashRing.hs
@@ -0,0 +1,189 @@
+-- | module: Data.HashRing
+-- copyright: (c) Michael S. Craig 2012
+-- license: BSD3
+-- maintainer: mkscrg@gmail.com
+-- stability: experimental
+-- portability: portable
+--
+-- An efficient implementation of consistent hashing, as described in
+--
+--    * David Karger et al.,
+--    \"/Consistent hashing and random trees: distributed caching protocols for relieving hot spots on the World Wide Web/\",
+--    29th Annual ACM Symposium on Theory,
+--    <http://dl.acm.org/citation.cfm?id=258660>
+--
+-- In distributed computing applications, it's usually necessary route messages
+-- to some group of N nodes in the network. Message locality, wherein messages
+-- of the same kind are routed to the same node, is often desirable. \"Normal\"
+-- hashing, where a message's node is determined by some hash function modulo
+-- N, has the undesirable property that adding or removing a node from the
+-- network causes the key sets of all other nodes to change drastically.
+-- In contrast, consistent hashing has the property that small changes to the
+-- size of the node set cause only small changes to key sets of the nodes.
+--
+-- This implementation is built on top of 'IntMap' and 'Set'. It provides
+-- /O(1)/ lookup functions as well as /O(min(log n, R))/ insertion and deletion
+-- functions, where /R/ is the number of replica nodes used in the ring (see
+-- 'empty').
+--
+-- The key space of the ring is the full range of 'Int' values. To insert a
+-- node, we generate (/R > 0/) keys by hashing the node with /R/ successive
+-- salts, and the node is referenced by those keys in the ring. To get a node
+-- for a message, we hash the message to an 'Int' value /k/ and find the
+-- smallest key /k'/ in the ring such that /k <= k'/. The node is the value
+-- referenced by /k'/. Higher values of /R/ give a more even distribution of
+-- keys to nodes but slow down insertions and deletions of nodes. /R/ is
+-- specified when constructing a 'HashRing' with 'empty', 'singleton', or
+-- 'fromList' and retrievable with 'replicas'.
+--
+-- The ability of 'HashRing' to fairly distribute messages among nodes relies
+-- on the implementations of 'Data.Hashable.hashWithSalt' for the message and
+-- node types. For example, the default implementation for
+-- 'Data.ByteString.ByteString' is non-uniform on short inputs, and so it's
+-- unsuitable for use with 'HashRing'. Reimplementing
+-- 'Data.Hashable.hashWithSalt' for your message and node types with a
+-- cryptographic hash function (like MD5 or SHA1 from the @cryptohash@ package)
+-- will give better results.
+
+module Data.HashRing (
+  -- * Map type
+  HashRing
+  -- * Construction
+, empty
+, singleton
+  -- * Operators
+, (!)
+  -- * Query
+, null
+, size
+, replicas
+, member
+, lookup
+, find
+  -- * Insertion
+, insert
+  -- * Deletion
+, delete
+  -- * Conversion
+, fromList
+, toList
+) where
+
+import Prelude hiding (lookup, null)
+import Data.Hashable (Hashable, hash, hashWithSalt)
+import Data.IntMap (IntMap)
+import qualified Data.IntMap as IM
+import Data.Set (Set)
+import qualified Data.Set as S
+
+
+-- | The constructor for this data type is not exported. See 'empty',
+-- 'singleton', or 'fromList'.
+--
+-- Note that 'HashRing' is parameterized by the node type and not by the
+-- message type. As made clear by the type signatures for '!', 'find', and
+-- 'lookup', any 'Hashable' type can be used as a message.
+data HashRing a = HashRing
+  { nodeMap :: IntMap a
+  , nodeSet :: Set a
+  , replicas :: Int
+  -- ^ Number of replica nodes (/R/) in the ring for each real node.
+  } deriving (Eq)
+
+
+instance Show a => Show (HashRing a) where
+  showsPrec d ring = showParen (d > 10)
+                   $ showString "fromList "
+                   . shows (replicas ring)
+                   . showString " "
+                   . shows (toList ring)
+
+instance (Read a, Ord a, Hashable a) => Read (HashRing a) where
+  readsPrec d = readParen (d > 10) $ \s -> do
+    ("fromList", s') <- lex s
+    (nreps, s'') <- reads s'
+    (nodes, s''') <- reads s''
+    return (fromList nreps nodes, s''')
+
+
+-- | Construct an empty ring with a specific /R/ value.
+empty :: Int -> HashRing a
+empty nreps = HashRing IM.empty S.empty nreps
+
+-- | Construct a single-node ring with a specific /R/ value.
+singleton :: (Ord a, Hashable a) => Int -> a -> HashRing a
+singleton nreps node = insert node $ empty nreps
+
+
+-- | @True@ if the ring is empty, @False@ otherwise.
+null :: HashRing a -> Bool
+null ring = S.null $ nodeSet ring
+
+-- | Number of nodes in the ring.
+size :: HashRing a -> Int
+size ring = S.size $ nodeSet ring
+
+-- | @True@ if the node is in the ring, @False@ otherwise.
+member :: Ord a => a -> HashRing a -> Bool
+member node ring = S.member node $ nodeSet ring
+
+
+-- | Get the node in the ring corresponding to a message, or @Nothing@ if the
+-- ring is empty.
+lookup :: Hashable b => b -> HashRing a -> Maybe a
+lookup msg ring = let nmap = nodeMap ring in if IM.null nmap
+  then Nothing
+  else Just $ case IM.splitLookup (hash msg) nmap of
+    (_, Just node, _) -> node
+    (_, _, submap) -> if IM.null submap
+      then snd $ IM.findMin nmap
+      else snd $ IM.findMin submap
+
+-- | Get the node in the ring corresponding to a message, or error if the ring
+-- is empty.
+find :: Hashable b => b -> HashRing a -> a
+find msg ring = case lookup msg ring of
+  Just x -> x
+  Nothing -> error "HashRing.find: empty hash ring"
+
+-- | Get the node in the ring corresponding to a message, or error if the ring
+-- is empty.
+(!) :: Hashable b => HashRing a -> b -> a
+ring ! msg = find msg ring
+
+
+-- | Add a node to the ring.
+insert :: (Ord a, Hashable a) => a -> HashRing a -> HashRing a
+insert node ring
+  | S.member node $ nodeSet ring = ring
+  | otherwise = ring
+    { nodeMap = foldr (\key nmap -> IM.insert key node nmap) (nodeMap ring)
+              $ take (replicas ring)
+              $ filter (\key -> not $ IM.member key (nodeMap ring))
+              $ nodeKeys node
+    , nodeSet = S.insert node (nodeSet ring) }
+
+-- | Remove a node from the ring.
+delete :: (Ord a, Hashable a) => a -> HashRing a -> HashRing a
+delete node ring
+  | not $ S.member node $ nodeSet ring = ring
+  | otherwise = ring
+    { nodeMap = foldr (\key nmap -> IM.delete key nmap) (nodeMap ring)
+              $ take (replicas ring)
+              $ filter (\key -> IM.lookup key (nodeMap ring) == Just node)
+              $ nodeKeys node
+    , nodeSet = S.delete node (nodeSet ring) }
+
+
+-- | Construct a ring from an /R/ value and a list of nodes.
+fromList :: (Ord a, Hashable a) => Int -> [a] -> HashRing a
+fromList nreps nodes = foldr insert (empty nreps) nodes
+
+-- | Construct a list containing the nodes in the ring.
+toList :: HashRing a -> [a]
+toList ring = S.toList $ nodeSet ring
+
+
+-- List of possible keys for a new node
+nodeKeys :: Hashable a => a -> [Int]
+nodeKeys node = map (\i -> hashWithSalt i node) [0..]
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,25 @@
+The following license covers this documentation, and the source code, except
+where otherwise indicated.
+
+Copyright (c) 2012, Michael S. Craig. 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.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "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 HOLDERS 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,6 @@
+# hashring
+
+Efficient [consistent hashing](http://en.wikipedia.org/wiki/Consistent_hashing)
+in Haskell.
+
+See the haddocks on [Hackage](http://hackage.haskell.org/package/hashring).
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,4 @@
+import Distribution.Simple
+
+main :: IO ()
+main = defaultMain
diff --git a/hashring.cabal b/hashring.cabal
new file mode 100644
--- /dev/null
+++ b/hashring.cabal
@@ -0,0 +1,46 @@
+name: hashring
+version: 0.0.0
+license: BSD3
+license-file: LICENSE.md
+author: Michael S. Craig <mkscrg@gmail.com>
+maintainer: Michael S. Craig <mkscrg@gmail.com>
+stability: experimental
+synopsis: Efficient consistent hashing.
+description:
+  An efficient implementation of consistent hashing. See the documentation for
+  @Data.HashRing@ for more info.
+homepage: https://github.com/mkscrg/hashring
+bug-reports: https://github.com/mkscrg/hashring/issues
+category: Data
+build-type: Simple
+cabal-version: >= 1.9.2
+extra-source-files: README.md
+
+library
+  exposed-modules: Data.HashRing
+  ghc-options: -Wall -O2
+  build-depends:
+    base >= 4 && < 5,
+    containers >= 0.4 && < 0.5,
+    hashable >= 1.1.1 && < 1.2
+
+test-suite tests
+  type: exitcode-stdio-1.0
+  main-is: Properties.hs
+  hs-source-dirs: tests
+  ghc-options: -Wall -O2 -fno-warn-orphans
+  build-depends:
+    base >= 4 && < 5,
+    hashring,
+    test-framework,
+    test-framework-quickcheck2,
+    QuickCheck
+
+source-repository head
+  type: git
+  location: git://github.com/mkscrg/hashring
+
+source-repository this
+  type: git
+  location: git://github.com/mkscrg/hashring
+  tag: 0.0.0
diff --git a/tests/Properties.hs b/tests/Properties.hs
new file mode 100644
--- /dev/null
+++ b/tests/Properties.hs
@@ -0,0 +1,119 @@
+{-# LANGUAGE FlexibleInstances, TypeSynonymInstances #-}
+
+module Main (main) where
+
+import Data.HashRing (HashRing, (!))
+import qualified Data.HashRing as R
+import Data.List ((\\), nub)
+import Data.Word (Word8)
+import Test.QuickCheck (Arbitrary (..), Gen, Property, (==>), listOf, resize)
+import Test.Framework (defaultMain, testGroup )
+import Test.Framework.Providers.QuickCheck2 (testProperty)
+
+
+main :: IO ()
+main = defaultMain
+  [ testGroup "basic properties"
+    [ testProperty "empty ring is null" pEmptyIsNull
+    , testProperty "nonempty rings are not null" pNonemptyNonNull
+    , testProperty "size is # of nodes" pSize
+    , testProperty "replicas is # of replicas" pReplicas
+    , testProperty "equality" pInequality ]
+  , testGroup "query properties"
+    [ testProperty "lookup in empty ring" pLookupEmpty
+    , testProperty "member identity" pMember
+    , testProperty "lookup identity" pLookupId
+    , testProperty "find identity" pFindId
+    , testProperty "index (bang) identity" pIndexId
+    , testProperty "lookup not Nothing" pLookupJust
+    , testProperty "lookup wraps" pLookupWrap ]
+  , testGroup "insert/delete properties"
+    [ testProperty "insert into empty" pSingleton
+    , testProperty "insert idempotency" pInsertIdem
+    , testProperty "insert then delete" pInsertDelete
+    , testProperty "delete nonmember" pDeleteNonMember ]
+  , testGroup "conversion properties"
+    [ testProperty "show/read identity" pShowRead
+    , testProperty "list conversion" pListConvert ] ]
+
+
+type IRing = HashRing Int
+newtype NReps = NReps Int deriving (Show)
+
+instance Arbitrary NReps where
+  arbitrary = fmap (NReps . (+1) . fromIntegral) (arbitrary :: Gen Word8)
+
+instance Arbitrary IRing where
+  arbitrary = do
+    NReps nreps <- arbitrary
+    nodes <- resize 100 $ listOf arbitrary
+    return $ R.fromList nreps nodes
+
+
+pEmptyIsNull :: NReps -> Bool
+pEmptyIsNull (NReps nreps) = R.null $ R.empty nreps
+
+pNonemptyNonNull :: IRing -> Property
+pNonemptyNonNull ring = R.size ring > 0 ==> not (R.null ring)
+
+pSize :: NReps -> [Int] -> Bool
+pSize (NReps nreps) nodes = go nodes $ R.empty nreps
+  where
+    go [] ring = R.size ring == length (nub nodes)
+    go (n:ns) ring = go ns $ R.insert n ring
+
+pReplicas :: NReps -> Bool
+pReplicas (NReps nreps) = R.replicas (R.empty nreps) == nreps
+
+pInequality :: NReps -> Int -> Bool
+pInequality (NReps nreps) node = R.singleton nreps node /= R.empty nreps
+
+
+pLookupEmpty :: Int -> NReps -> Bool
+pLookupEmpty node (NReps nreps) =
+  R.lookup node (R.empty nreps :: IRing) == Nothing
+
+pMember :: Int -> IRing -> Bool
+pMember node ring = R.member node (R.insert node ring) == True
+
+pLookupId :: Int -> IRing -> Bool
+pLookupId msgnode ring = R.lookup msgnode (R.insert msgnode ring) == Just msgnode
+
+pFindId :: Int -> IRing -> Bool
+pFindId msgnode ring = R.find msgnode (R.insert msgnode ring) == msgnode
+
+pIndexId :: Int -> IRing -> Bool
+pIndexId msgnode ring = R.insert msgnode ring ! msgnode == msgnode
+
+pLookupJust :: Int -> IRing -> Property
+pLookupJust msg ring = not (R.null ring) ==> R.lookup msg ring /= Nothing
+
+pLookupWrap :: Int -> NReps -> Property
+pLookupWrap msgnode (NReps nreps) = msgnode /= maxBound ==>
+  R.lookup (msgnode + 1) (R.insert msgnode $ R.empty nreps) == Just msgnode
+
+
+pSingleton :: NReps -> Int -> Bool
+pSingleton (NReps nreps) node =
+  R.insert node (R.empty nreps) == R.singleton nreps node
+
+pInsertIdem :: Int -> IRing -> Bool
+pInsertIdem node ring =
+  R.insert node (R.insert node ring) == R.insert node ring
+
+pInsertDelete :: Int -> IRing -> Bool
+pInsertDelete node ring = ring == R.delete node (R.insert node ring)
+
+pDeleteNonMember :: Int -> IRing -> Property
+pDeleteNonMember node ring =
+  not (R.member node ring) ==> R.delete node ring == ring
+
+
+pShowRead :: IRing -> Bool
+pShowRead ring = ring == read (show ring)
+
+pListConvert :: NReps -> [Int] -> Bool
+pListConvert (NReps nreps) ints =
+  let innodes = nub ints
+      outnodes = R.toList $ R.fromList nreps innodes
+  in innodes \\ outnodes == [] && outnodes \\ innodes == []
