diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 # Revision history for cached-io
 
+## 1.3.2.0
+
+- Add `Functor` instance for `Cached`.
+
 ## 1.3.1.0
 
 - Add `cachedSTM` versions of all bindings.
diff --git a/cached-io.cabal b/cached-io.cabal
--- a/cached-io.cabal
+++ b/cached-io.cabal
@@ -1,6 +1,6 @@
 cabal-version:   2.2
 name:            cached-io
-version:         1.3.1.0
+version:         1.3.2.0
 synopsis:        A simple library to cache IO actions
 description:
   Provides functions that convert an IO action into a cached one by storing the
diff --git a/src/Control/Concurrent/CachedIO.hs b/src/Control/Concurrent/CachedIO.hs
--- a/src/Control/Concurrent/CachedIO.hs
+++ b/src/Control/Concurrent/CachedIO.hs
@@ -1,3 +1,6 @@
+{-# LANGUAGE DeriveFunctor #-}
+{-# LANGUAGE DerivingStrategies #-}
+
 -- | Example usage:
 --
 -- > -- Downloads a large payload from an external data store.
@@ -16,12 +19,15 @@
 --   it. In that case, the stored value will be returned in the meantime.
 module Control.Concurrent.CachedIO
   ( Cached (..),
+    Config (..),
+    Timestamped,
 
     -- * IO
     cachedIO,
     cachedIOWith,
     cachedIO',
     cachedIOWith',
+    cachedIOFromConfig,
 
     -- * STM
     -- $stm
@@ -29,6 +35,7 @@
     cachedSTMWith,
     cachedSTM',
     cachedSTMWith',
+    cachedSTMFromConfig,
   )
 where
 
@@ -49,9 +56,22 @@
 --
 -- Note that using 'Control.Monad.join' when the cached action and the outer monad are the same will ignore caching.
 newtype Cached m a = Cached {runCached :: m a}
+  deriving stock (Functor)
 
 data State a = Uninitialized | Initializing | Updating a | Fresh UTCTime a
 
+-- | A cached value and when it was cached.
+type Timestamped a = (UTCTime, a)
+
+data Config m a = Config
+  { -- | @isFresh now (timestamp, value)@ should return 'False' if @value@ is
+    -- considered to have expired, 'True' otherwise.
+    isFresh :: UTCTime -> Timestamped a -> Bool,
+    -- | Produces a new value. The currently-cached value is supplied if it is
+    -- available.
+    refreshAction :: Maybe (Timestamped a) -> m a
+  }
+
 -- | Cache an IO action, producing a version of this IO action that is cached
 -- for 'interval' seconds. The cache begins uninitialized.
 --
@@ -105,16 +125,27 @@
 cachedIOWith' ::
   (MonadIO m, MonadIO t, MonadCatch t) =>
   -- | Test function:
-  -- If 'isCacheStillFresh' 'lastUpdated' 'now' returns 'True'
+  -- If @isCacheStillFresh lastUpdated now@ returns 'True'
   -- the cache is considered still fresh and returns the cached IO action
   (UTCTime -> UTCTime -> Bool) ->
   -- | Action to cache. The stale value and its refresh date
   -- are passed so that the action can perform external staleness checks
   (Maybe (UTCTime, a) -> t a) ->
   m (Cached t a)
-cachedIOWith' isCacheStillFresh refreshAction =
-  liftIO . atomically $ cachedSTMWith' isCacheStillFresh refreshAction
+cachedIOWith' isFresh' refreshAction' =
+  cachedIOFromConfig
+    Config
+      { isFresh = \now (lastUpdated, _) -> isFresh' lastUpdated now,
+        refreshAction = refreshAction'
+      }
 
+-- | Cache an IO action; the cache begins uninitialized.
+--
+-- The outer IO is responsible for setting up the cache. Use the inner one to
+-- either get the cached value or refresh
+cachedIOFromConfig :: (MonadIO m, MonadIO t, MonadCatch t) => Config t a -> m (Cached t a)
+cachedIOFromConfig = liftIO . atomically . cachedSTMFromConfig
+
 -- $stm
 --
 -- The following actions are the exactly same as the 'cachedIO' versions, except
@@ -159,14 +190,26 @@
 cachedSTMWith' ::
   (MonadIO m, MonadCatch m) =>
   -- | Test function:
-  -- If @'isCacheStillFresh' lastUpdated now@ returns 'True'
+  -- If @isCacheStillFresh lastUpdated now@ returns 'True'
   -- the cache is considered still fresh and returns the cached IO action
   (UTCTime -> UTCTime -> Bool) ->
   -- | Action to cache. The stale value and its refresh date
   -- are passed so that the action can perform external staleness checks
   (Maybe (UTCTime, a) -> m a) ->
   STM (Cached m a)
-cachedSTMWith' isCacheStillFresh refreshAction = do
+cachedSTMWith' isFresh' refreshAction' =
+  cachedSTMFromConfig
+    Config
+      { isFresh = \now (lastUpdated, _) -> isFresh' lastUpdated now,
+        refreshAction = refreshAction'
+      }
+
+-- | Set up a cached IO action in a transaction; the cache begins uninitialized.
+cachedSTMFromConfig ::
+  (MonadIO m, MonadCatch m) =>
+  Config m a ->
+  STM (Cached m a)
+cachedSTMFromConfig config = do
   cachedT <- newTVar Uninitialized
   pure . Cached $ do
     now <- liftIO getCurrentTime
@@ -175,7 +218,7 @@
       case cached of
         previousState@(Fresh lastUpdated value)
           -- There's data in the cache and it's recent. Just return.
-          | isCacheStillFresh lastUpdated now -> pure (pure value)
+          | isFresh config now (lastUpdated, value) -> pure (pure value)
           -- There's data in the cache, but it's stale. Update the cache state
           -- to prevent a second thread from also executing the action. The second
           -- thread will get the stale data instead.
@@ -198,7 +241,7 @@
             Fresh lastUpdated value -> Just (lastUpdated, value)
             _ -> Nothing
       newValue <-
-        refreshAction previous
+        refreshAction config previous
           `onException` liftIO (atomically (writeTVar cachedT previousState))
       liftIO $ do
         now <- getCurrentTime
