diff --git a/changelog.md b/changelog.md
--- a/changelog.md
+++ b/changelog.md
@@ -1,3 +1,8 @@
+# Version 0.4.1
+
+* `ExchangeRate` is now a `Category`.
+
+
 # Version 0.4
 
 * BREAKING CHANGE. COMPILER WON'T COMPLAIN. HUMAN INTERVENTION POTENTIALLY
diff --git a/safe-money.cabal b/safe-money.cabal
--- a/safe-money.cabal
+++ b/safe-money.cabal
@@ -1,5 +1,5 @@
 name: safe-money
-version: 0.4
+version: 0.4.1
 license: BSD3
 license-file: LICENSE
 copyright: Copyright (c) Renzo Carbonara 2016-2017
diff --git a/src/Money.hs b/src/Money.hs
--- a/src/Money.hs
+++ b/src/Money.hs
@@ -14,6 +14,10 @@
 -- a new 'I.Scale' instance, and /voilà/. If you want to add a new currency to the
 -- out-of-the-box offer, please request so in
 -- https://github.com/k0001/safe-money/issues and the authors will see to it.
+--
+-- This module offers plenty of documentation, but for a deep explanation of
+-- how all of the pieces fit together, please read
+-- https://ren.zone/articles/safe-money
 module Money
  ( -- * Dense monetary values
    I.Dense
diff --git a/src/Money/Internal.hs b/src/Money/Internal.hs
--- a/src/Money/Internal.hs
+++ b/src/Money/Internal.hs
@@ -71,6 +71,7 @@
  ) where
 
 import Control.Applicative ((<|>), empty)
+import Control.Category (Category((.), id))
 import Control.Monad ((<=<), guard, when)
 import Data.Constraint (Dict(Dict))
 import Data.Monoid ((<>))
@@ -82,7 +83,7 @@
 import GHC.TypeLits
   (Symbol, SomeSymbol(..), Nat, SomeNat(..), CmpNat, KnownSymbol, KnownNat,
    natVal, someNatVal, symbolVal, someSymbolVal)
-import Prelude hiding (round, ceiling, floor, truncate)
+import Prelude hiding ((.), round, ceiling, floor, truncate)
 import qualified Prelude
 import qualified Text.ParserCombinators.ReadPrec as ReadPrec
 import qualified Text.ParserCombinators.ReadP as ReadP
@@ -353,7 +354,7 @@
 -- then the following holds:
 --
 -- @
--- 'ceiling' x == (y, 'Just' z)
+-- 'ceiling' x == (y, z)
 -- @
 --
 -- @
@@ -361,7 +362,7 @@
 -- @
 --
 -- @
--- z < 'zero'
+-- z < 0
 -- @
 --
 -- Proof that 'ceiling' doesn't lose money:
@@ -391,7 +392,7 @@
 -- then the following holds:
 --
 -- @
--- 'floor' x == (y, 'Just' z)
+-- 'floor' x == (y, z)
 -- @
 --
 -- @
@@ -399,7 +400,7 @@
 -- @
 --
 -- @
--- z > 'zero'
+-- z > 0
 -- @
 --
 -- Proof that 'floor' doesn't lose money:
@@ -582,6 +583,42 @@
 newtype ExchangeRate (src :: Symbol) (dst :: Symbol) = ExchangeRate Rational
   deriving (Eq, Ord, GHC.Generic)
 
+
+-- | Composition of 'ExchangeRate's multiplies exchange rates together:
+--
+-- @
+-- 'fromExchangeRate' x * 'fromExchangeRate' y  ==  'fromExchangeRate' (x . y)
+-- @
+--
+-- Identity:
+--
+-- @
+-- x  ==  x . id  ==  id . x
+-- @
+--
+-- Associativity:
+--
+-- @
+-- x . y . z  ==  x . (y . z)  ==  (x . y) . z
+-- @
+--
+-- Conmutativity (provided the types allow for composition):
+--
+-- @
+-- x . y  ==  y . x
+-- @
+--
+-- Multiplicative inverse:
+--
+-- @
+-- 1  ==  'fromExchangeRate' (x . 'flipExchangeRate' x)
+-- @
+instance Category ExchangeRate where
+  id = ExchangeRate 1
+  {-# INLINE id #-}
+  ExchangeRate a . ExchangeRate b = ExchangeRate (a * b)
+  {-# INLINE (.) #-}
+
 instance forall src dst.
   ( KnownSymbol src, KnownSymbol dst
   ) => Show (ExchangeRate src dst) where
@@ -624,7 +661,8 @@
 
 -- | Flip the direction of an 'ExchangeRate'.
 --
--- Identity law:
+-- This function retuns the multiplicative inverse of the given 'ExchangeRate',
+-- leading to the following identity law:
 --
 -- @
 -- 'flipExchangeRate' . 'flipExchangeRate'   ==  'id'
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -8,11 +8,13 @@
 
 module Main where
 
+import Control.Category (Category((.), id))
 import qualified Data.ByteString.Lazy as BSL
 import Data.Maybe (catMaybes, isJust, isNothing)
 import Data.Proxy (Proxy(Proxy))
 import Data.Ratio (numerator, denominator)
 import GHC.TypeLits (Nat, Symbol, KnownSymbol, symbolVal)
+import Prelude hiding ((.), id)
 import qualified Test.Tasty as Tasty
 import qualified Test.Tasty.Runners as Tasty
 import Test.Tasty.QuickCheck ((===), (==>), (.&&.))
@@ -446,7 +448,24 @@
 testExchangeRate ps pd =
   Tasty.testGroup ("ExchangeRate " ++ show (symbolVal ps) ++ " "
                                    ++ show (symbolVal pd))
-  [ QC.testProperty "read . show == id" $
+  [ QC.testProperty "Category: left identity" $
+      QC.forAll QC.arbitrary $ \(xr :: Money.ExchangeRate src dst) ->
+         xr === id . xr
+  , QC.testProperty "Category: right identity" $
+      QC.forAll QC.arbitrary $ \(xr :: Money.ExchangeRate src dst) ->
+         xr === xr . id
+  , QC.testProperty "Category: composition with inverse" $
+      QC.forAll QC.arbitrary $ \(xr1 :: Money.ExchangeRate src dst) ->
+         (1 === Money.fromExchangeRate (xr1 . Money.flipExchangeRate xr1)) .&&.
+         (1 === Money.fromExchangeRate (Money.flipExchangeRate xr1 . xr1))
+  , QC.testProperty "Category: composition with other" $
+      QC.forAll QC.arbitrary $ \(xr1 :: Money.ExchangeRate src dst,
+                                 xr2 :: Money.ExchangeRate dst src) ->
+         let a = Money.fromExchangeRate xr1 * Money.fromExchangeRate xr2
+         in (a === Money.fromExchangeRate (xr1 . xr2)) .&&.
+            (a === Money.fromExchangeRate (xr2 . xr1))
+
+  , QC.testProperty "read . show == id" $
       QC.forAll QC.arbitrary $ \(xr :: Money.ExchangeRate src dst) ->
          xr === read (show xr)
   , QC.testProperty "read . show . Just == Just" $
