Repository: whitfin/cachex (elixir)

Issue:
Remove Cachex.Policy in favour of hooks
There is no real point of policies and limits in the current implementation of Cachex.

Each existing limit simply spawns hooks to handle the management of a cache, so we should remove the modules for policies and simply document adding of the hooks instead. This is quite a big change, but it should make a lot more sense on completion.

Pull request:
Remove :stats and :limit from the list of cache options
This fixes #368.

There's a lot in here, but the gist is that `:stats` and `:limit` have been completely removed in favour of simply providing the hooks directly. This is a much smaller implementation, and is easier to understand (because we're guiding everyone to hooks). 

This also introduces a new cache action via `Cachex.prune/3` which can be used to manually prune a cache to a size. This is used by the hooks under the hood. 

Hook records have also had `:state` => `:args` to better show that they can have modified state, more inline with `GenServer`. 

Patch:
diff --git a/README.md b/README.md
index b3cdaec..e14ee9a 100644
--- a/README.md
+++ b/README.md
@@ -58,24 +58,7 @@ iex(4)> Cachex.get!(:missing_cache, "key")
     (cachex) lib/cachex.ex:249: Cachex.get!/3
 ```
 
-Generally you should use the non-`!` versions to be more explicit in your code, but the `!` version exists for convenience and to make assertions easier in unit testing.
-
-## Options
-
-Caches also accept several options at startup to toggle various behaviour. These options are defined on a per-cache basis and can be used to control the features available to the cache:
-
-|      Options     |          Values          |                             Description                            |
-|:----------------:|:------------------------:|:------------------------------------------------------------------:|
-|     commands     |      map or keyword      |       A collection of custom commands to attach to the cache.      |
-|    expiration    |      `expiration()`      |      An expiration options record imported from `Cachex.Spec`.     |
-|       hooks      |     list of `hook()`     |        A list of execution hooks to listen on cache actions.       |
-|       limit      |    a `limit()` record    |    An integer or Limit struct to define the bounds of this cache.  |
-|       stats      |          boolean         |         Whether to track statistics for this cache or not.         |
-|   transactions   |          boolean         |           Whether to turn on transactions at cache start.          |
-|      warmers     |    list of `warmer()`    |           A list of cache warmers to enable on the cache.
-
-
-For further information or examples on these features and options, please see the [documentation](https://hexdocs.pm/cachex).
+Generally you should use the non-`!` versions to be more explicit in your code, but the `!` version exists for convenience and to make assertions easier in unit testing. For further information or examples on supported features and options, please see the [documentation](https://hexdocs.pm/cachex).
 
 ## Benchmarks
 
diff --git a/docs/management/limiting-caches.md b/docs/management/limiting-caches.md
index d49fc9f..5cdec17 100644
--- a/docs/management/limiting-caches.md
+++ b/docs/management/limiting-caches.md
@@ -1,43 +1,121 @@
 # Limiting Caches
 
-Cache limits are restrictions on a cache to ensure that it stays within given bounds. Currently these limits are based around the number of entries inside a cache, but there are plans to add new policies in future (for example basing the limits on memory spaces).
+Cache limits are restrictions on a cache to ensure that it stays within given bounds. The limits currently shipped inside Cachex are based around the number of entries inside a cache, but there are plans to add new policies in future (for example basing the limits on memory spaces). You even even write your own!
 
-## Configuration
+## Manual Pruning
 
-Limits are defined at cache startup and cannot be changed at this point in time. You can provide either an integer or a `limit` record to the `:limit` option in the Cachex interface.
+The main entrypoint to cache limitation included in Cachex is `Cachex.prune/3`, which provides a Least Recently Written (LRW) implementation of pruning a cache. This means that we calculate the oldest entries, and remove them from the cache in order to make room for new entries. It should be noted that "oldest" in this context means "those written or updated longest ago".
+
+You can trigger a pruning manually via `Cachex.prune/3`, passing the maximum size of a cache to shrink to:
+
+```elixir
+# start a new cache
+Cachex.start(:my_cache)
+
+# insert 100 keys
+for i <- 1..100 do
+  Cachex.put!(:my_cache, i, i)
+end
+
+# guarantee we have 100 keys in the cache
+{ :ok,  100 } = Cachex.size(:my_cache)
+
+# trigger a pruning down to 50 keys only
+{ :ok, true } = Cachex.prune(:my_cache, 50, reclaim: 0)
+
+# verify that we're down to 50 keys
+{ :ok,   50 } = Cachex.size(:my_cache)
+```
+
+The `:reclaim` option can be used to reduce thrashing, by evicting an additional number of entries. In the case above the next write would cause the cache to once again need pruning, and then so on. The `:reclaim` option accepts a percentage (as a decimal) of extra keys to evict, which gives us a buffer between pruning of a cache.
+
+To demonstrate this we can run the same example as above, except using a `:reclaim` of `0.1` (the default). This time we'll be left with 45 keys instead of 50, as we reclaimed an extra 10% of the table (`50 * 0.1 = 5`):
+
+```elixir
+# start a new cache
+Cachex.start(:my_cache)
+
+# insert 100 keys
+for i <- 1..100 do
+  Cachex.put!(:my_cache, i, i)
+end
+
+# guarantee we have 100 keys in the cache
+{ :ok,  100 } = Cachex.size(:my_cache)
+
+# trigger a pruning down to 50 keys, reclaiming 10%
+{ :ok, true } = Cachex.prune(:my_cache, 50, reclaim: 0.1)
+
+# verify that we're down to 45 keys
+{ :ok,   45 } = Cachex.size(:my_cache)
+```
+
+It is almost never a good idea to set `reclaim: 0` unless you have very specific use cases, so if you don't it's recommended to leave `:reclaim` at the default value - it was only used above for example purposes.
+
+## Lifecycle Pruning
+
+Although you can manually prune a cache, in reality this isn't particularly useful as you want to be able to continually monitor a cache's size. For this reason, Cachex includes several lifecycle hooks to trigger `Cachex.Limit` automatically. This will give you a monitored cache size, easily configured at cache startup:
 
 ```elixir
 # include records
 import Cachex.Spec
 
-# maximum 500 entries, default eviction, default trim
-Cachex.start(:my_cache, [ limit: 500 ])
-
-# maximum 500 entries, LRW eviction, trim to 250
-Cachex.start(:my_cache, [ limit: limit(size: 500, policy: Cachex.Policy.LRW, reclaim: 0.5) ])
+# maximum 500 entries, LRW eviction, default trim
+Cachex.start(:my_cache,
+  hooks: [
+    hook(module: Cachex.Limit.Scheduled, args: {
+      500,  # setting cache max size
+      [],   # options for `Cachex.prune/3`
+      []    # options for `Cachex.Limit.Scheduled`
+    })
+  ]
+)
 ```
 
-A `limit` record consists (currently) of only 4 fields which dictate a limit and how it should be enforced. This allows the user to customize their eviction without getting too low-level. Below is an example structure, which demonstrates what an integer `:limit` parameter would unpack to internally.
+This will spawn a cache hook to continually prune your cache periodically, based on the options you provided. You can pass options for `Cachex.prune/3` as the second element of the `args` tuple, and customize the hook itself in the third element. Currently the only supported parameter for the hook is `:frequency`, which defaults to `1000` (one second). Please see the documentation for an updated list of supported configuration.
+
+If you need more exact timing you can opt to use `Cachex.Limit.Evented` rather than `Cachex.Limit.Scheduled`, which will react to hook events inside a cache instead of running on a schedule:
 
 ```elixir
-limit(
-  # the limit provided
-  size: 500,
-  # the policy to use for eviction
-  policy: Cachex.Policy.LRW,
-  # how much to reclaim on bound expiration
-  reclaim: 0.1,
-  # options to pass to the policy
-  options: []
+# include records
+import Cachex.Spec
+
+# maximum 500 entries, LRW eviction, default trim
+Cachex.start(:my_cache,
+  hooks: [
+    hook(module: Cachex.Limit.Evented, args: {
+      500,  # setting cache max size
+      []    # options for `Cachex.prune/3`
+    })
+  ]
 )
 ```
 
-To expound a little on the above, it defines that the cache should aim to store no more than `500` entries (which is user defined). If the cache key space goes above this number, it should evict `50` of the entries in the cache as chosen by the provided `:policy`. The amount `50` is dictated by the `:reclaim` option, which is essentially a percentage of the cache to evict on hitting the bounds. This value much match `1 >= value >= 0` in order to be accepted and override the default (due to being a percentage).
+This is a much more accurate policy, but has a much higher memory and CPU overhead due to hooking into the main lifecycle events. If you can, it's recommended to use the scheduling approach for this reason.
 
-## Policies
+It should be hopefully be evident from the above, but lifecycle pruning is not instant - in general it is extremely quick, however if you have a cache limit of 500 keys and you add 500,000 keys, the cleanup does take a few hundred milliseconds to occur (that's a lot to clean). This shouldn't affect most users, but it is something to point out and be aware of.
 
-The policy `Cachex.Policy.LRW` above is a built-in Cachex eviction policy which removes the oldest values first. This means that we calculate the first `N` oldest entries, where `N` is roughly equal to `limit * reclaim`, and remove them from the cache in order to make room for new entries. It should be noted that "oldest" in this context means "those written or updated longest ago". This is currently the only policy implemented within Cachex, although it's likely that more will follow (and you can write them yourself too).
+## LRU Style Pruning
 
-You should be aware that eviction is not instant - it happens in reaction to events which are additive to the cache and is extremely quick, however if you have a cache limit of 500 keys and you add 500,000 keys, the cleanup does take a few hundred milliseconds to occur (that's a lot to clean). This shouldn't affect most users, but it is something to point out and be aware of.
+In addition to the two lifecycle controls for LRW based caching, Cachex v4.x also includes a naive solution for those who wish to use Least Recently Used (LRU) based approaches via `Cachex.Limit.Accessed`.
+
+This is done as an extension of LRW caching by attaching a small lifecycle hook to update the access time of each record, thus making the existing LRW hooks able to handle them as usual. Due to this you can freely choose either of the two LRW approaches above, by placing the LRW access hook *before* it in the cache initialization:
+
+```elixir
+# include records
+import Cachex.Spec
+
+# maximum 500 entries, LRW eviction, default trim
+Cachex.start(:my_cache,
+  hooks: [
+    hook(module: Cachex.Limit.Accessed),
+    hook(module: Cachex.Limit.Scheduled, args: {
+      500,  # setting cache max size
+      [],   # options for `Cachex.prune/3`
+      []    # options for `Cachex.Limit.Scheduled`
+    })
+  ]
+)
+```
 
-It should be noted that although LRW is the only policy implemented at this time, you can control LRU policies by using `Cachex.touch/2` to do a write on a key without affecting the value or TTL. Using `Cachex.touch/2` alongside the LRW policy is likely how an LRU policy would work regardless.
+As you might expect, there is a fair cost to this as `Cachex.Limit.Accessed` must listen to and act on cache events when a key is accessed. This can result in heavy read/write activity within a cache. For this reason it's recommended to operate using LRW when possible, and LRU only as absolutely necessary.
diff --git a/docs/management/stats-gathering.md b/docs/management/stats-gathering.md
new file mode 100644
index 0000000..94359b3
--- /dev/null
+++ b/docs/management/stats-gathering.md
@@ -0,0 +1,56 @@
+# Gathering Statistics
+
+Cachex includes basic support for tracking statistics in a cache, so you can look at things like throughput and hit/miss rates. This is provided via the `Cachex.Stats` hook implementation.
+
+## Configuration
+
+As of Cachex v4.x this is configured as a hook during cache initialization:
+
+```elixir
+# include records
+import Cachex.Spec
+
+# create a cache with stats
+Cachex.start(:my_cache,
+  hooks: [
+    hook(module: Cachex.Stats)
+  ]
+)
+
+# insert 100 keys
+for i <- 1..100 do
+  Cachex.put!(:my_cache, i, i)
+end
+
+# generate both a cache hit and a miss
+{ :ok,   1 } = Cachex.get(:my_cache, 1)
+{ :ok, nil } = Cachex.get(:my_cache, 101)
+
+# print stats
+:my_cache
+|> Cachex.stats!()
+|> IO.inspect
+```
+
+Running this will give you a map of various statistics based on the actions and operations taken by your cache.
+
+## Example Statistics
+
+The statistics map returned by `Cachex.stats/2` should look something like the example below (at the time of writing):
+
+```elixir
+%{
+  meta: %{creation_date: 1726777631670},
+  hits: 1,
+  misses: 1,
+  hit_rate: 50.0,
+  miss_rate: 50.0,
+  calls: %{get: 2, put: 100},
+  operations: 102,
+  writes: 100
+}
+```
+
+As you can see, we see the breakdown of calls to the cache, the hit/miss rate, the total writes to a cache, etc. This is useful when gauging how much time your cache is actually saving and allows you to determine that everything is working as intended.
+
+It should be noted that the output format of `Cachex.stats/2` is *not* considered part of the Public API for backwards compatibility; the shape of this may change as and when it's necessary to do so.
diff --git a/lib/cachex.ex b/lib/cachex.ex
index 8abda87..c950e04 100644
--- a/lib/cachex.ex
+++ b/lib/cachex.ex
@@ -81,6 +81,7 @@ defmodule Cachex do
     invoke: [3, 4],
     keys: [1, 2],
     persist: [2, 3],
+    prune: [2, 3],
     purge: [1, 2],
     put: [3, 4],
     put_many: [2, 3],
@@ -201,31 +202,6 @@ defmodule Cachex do
 
       Please see the `Cachex.Spec.hook/1` documentation for further customization options.
 
-    * `:limit`
-
-      A cache limit provides a maximum size to cap the cache keyspace at. This should
-      be either a positive integer, or a valid `:limit` record provided by `Cachex.Spec`.
-      Internally a provided integer will just be coerced to a `:limit` record with some
-      default values set.
-
-          iex> import Cachex.Spec
-          ...>
-          ...> Cachex.start_link(:my_cache, [
-          ...>   # simple limit
-          ...>   limit: 500,
-          ...>
-          ...>   # complex limit
-          ...>   limit: limit(
-          ...>     size: 500,
-          ...>     policy: Cachex.Policy.LRW,
-          ...>     reclaim: 0.5,
-          ...>     options: []
-          ...>   )
-          ...> ])
-          { :ok, _pid }
-
-      Please see the `Cachex.Spec.limit/1` documentation for further customization options.
-
     * `:ordered`
 
       This option will specify whether this cache should enable ETS ordering, which can
@@ -253,17 +229,6 @@ defmodule Cachex do
 
       Please see the `Cachex.Spec.router/1` documentation for further customization options.
 
-    * `:stats`
-
-      This option can be used to toggle statistics gathering for a cache. This is a
-      shorthand option to avoid attaching the `Cachex.Stats` hook manually. Statistics
-      gathering has very minor overhead due to being implemented as a hook,
-
-      Stats can be retrieve from a running cache by using `Cachex.stats/2`.
-
-          iex> Cachex.start_link(:my_cache, [ stats: true ])
-          { :ok, _pid }
-
     * `:transactions`
 
       This option will specify whether this cache should have transactions and row
@@ -907,6 +872,52 @@ defmodule Cachex do
   def persist(cache, key, options \\ []) when is_list(options),
     do: expire(cache, key, nil, via({:persist, [key, options]}, options))
 
+  @doc """
+  Prunes to a maximum size of records in a cache.
+
+  Pruning is done via a Least Recently Written (LRW) approach, determined by the
+  modification time inside each cache record to avoid storing additional state.
+
+  For full details on this feature, please see the section of the documentation
+  related to limitation of caches.
+
+  ## Options
+
+    * `:batch_size`
+
+      Allows customization of the internal batching when paginating the cursor
+      coming back from ETS. It's unlikely this will ever need changing.
+
+    * `:reclaim`
+
+      Provides control over thrashing by evicting an additonal number of cache
+      entries beyond the maximum size. This option accepts a percentage (as a
+      decimal) of extra keys to evict, to provide buffer between pruning passes.
+      Defaults to `0.1` (i.e. 10%).
+
+  ## Examples
+
+      iex> Cachex.put(:my_cache, "key1", "value1")
+      { :ok, true }
+
+      iex> :timer.sleep(1)
+      :ok
+
+      iex> Cachex.put(:my_cache, "key2", "value2")
+      { :ok, true }
+
+      iex> Cachex.prune(:my_cache, 1, reclaim: 0)
+      { :ok, true }
+
+      iex> Cachex.keys(:my_cache)
+      { :ok, [ "key2"] }
+
+  """
+  @spec prune(Cachex.t(), integer, Keyword.t()) :: {status, boolean}
+  def prune(cache, size, options \\ [])
+      when is_positive_integer(size) and is_list(options),
+      do: Router.route(cache, {:prune, [size, options]})
+
   @doc """
   Triggers a cleanup of all expired entries in a cache.
 
@@ -1146,12 +1157,6 @@ defmodule Cachex do
   This will only provide statistics if the `:stats` option was
   provided on cache startup in `start_link/2`.
 
-  ## Options
-
-    * `:for`
-
-      Allows customization of exactly which statistics to retrieve.
-
   ## Examples
 
       iex> Cachex.stats(:my_cache)
diff --git a/lib/cachex/actions/prune.ex b/lib/cachex/actions/prune.ex
new file mode 100644
index 0000000..95e114a
--- /dev/null
+++ b/lib/cachex/actions/prune.ex
@@ -0,0 +1,123 @@
+defmodule Cachex.Actions.Prune do
+  @moduledoc false
+  # Command module to allow pruning a cache to a maximum size.
+  #
+  # This command will trigger an LRW-style pruning of a cache based on
+  # the provided maximum value. Various controls are provided on how to
+  # exactly prune the table.
+  #
+  # This command is used by the various limit hooks provided by Cachex.
+  alias Cachex.Actions.Stream, as: CachexStream
+  alias Cachex.Query
+  alias Cachex.Services.Informant
+
+  # add required imports
+  import Cachex.Spec
+
+  # compile our match to avoid recalculating
+  @query Query.build(output: {:key, :modified})
+
+  ##############
+  # Public API #
+  ##############
+
+  @doc """
+  Prunes cache keyspsace to the provided amount.
+
+  This function will enforce cache bounds using a least recently written (LRW)
+  eviction policy. It will trigger a Janitor purge to clear expired records
+  before attempting to trim older cache entries.
+  """
+  def execute(cache() = cache, size, options) do
+    batch_size =
+      case Keyword.get(options, :batch_size, 100) do
+        val when val < 0 -> 100
+        val -> val
+      end
+
+    reclaim = Keyword.get(options, :reclaim, 0.1)
+    reclaim_bound = round(size * reclaim)
+
+    case Cachex.size!(cache, const(:notify_false)) do
+      cache_size when cache_size <= size ->
+        notify_worker(0, cache)
+
+      cache_size ->
+        cache_size
+        |> calculate_reclaim(size, reclaim_bound)
+        |> calculate_poffset(cache)
+        |> erase_lower_bound(cache, batch_size)
+        |> notify_worker(cache)
+    end
+
+    {:ok, true}
+  end
+
+  ###############
+  # Private API #
+  ###############
+
+  # Calculates the space to reclaim inside a cache.
+  #
+  # This is a function of the maximum cache size, the reclaim bound and the
+  # current size of the cache. A positive result from this function means that
+  # we need to carry out evictions, whereas a negative results means that the
+  # cache is currently underpopulated.
+  defp calculate_reclaim(current_size, size, reclaim_bound),
+    do: (size - reclaim_bound - current_size) * -1
+
+  # Calculates the purge offset of a cache.
+  #
+  # Basically this means that if the cache is overpopulated, we would trigger
+  # a Janitor purge to see if expirations bring the cache back under the max
+  # size limits. The resulting amount of removed records is then offset against
+  # the reclaim space, meaning that a positive result require us to carry out
+  # further evictions manually down the chain.
+  defp calculate_poffset(reclaim_space, cache) when reclaim_space > 0,
+    do: reclaim_space - Cachex.purge!(cache)
+
+  # Erases the least recently written records up to the offset limit.
+  #
+  # If the provided offset is not positive we don't do anything as it signals that
+  # the cache is already within the correctly sized limits so we just pass through
+  # as a no-op.
+  #
+  # In the case the offset is positive, it represents the number of entries we need
+  # to remove from the cache table. We do this by traversing the underlying ETS table,
+  # which only selects the key and touch time as a minor optimization. The key is
+  # naturally required when it comes to removing the document, and the touch time is
+  # used to determine the sort order required for LRW.
+  defp erase_lower_bound(offset, cache, batch) when offset > 0 do
+    with {:ok, stream} <- CachexStream.execute(cache, @query, batch_size: batch) do
+      cache(name: name) = cache
+
+      stream
+      |> Enum.sort(fn {_k1, t1}, {_k2, t2} -> t1 < t2 end)
+      |> Enum.take(offset)
+      |> Enum.each(fn {k, _t} -> :ets.delete(name, k) end)
+
+      offset
+    end
+  end
+
+  defp erase_lower_bound(offset, _state, _batch),
+    do: offset
+
+  # Broadcasts the number of removed entries to the cache hooks.
+  #
+  # If the offset is not positive we didn't have to remove anything and so we
+  # don't broadcast any results. An 0 Tuple is returned just to keep compatibility
+  # with the response type from `Informant.broadcast/3`.
+  #
+  # It should be noted that we use a `:clear` action here as these evictions are
+  # based on size and not on expiration. The evictions done during the purge earlier
+  # in the pipeline are reported separately and we're only reporting the delta at this
+  # point in time. Therefore remember that it's important that we're ignoring the
+  # results of `clear()` and `purge()` calls in this hook, otherwise we would end
+  # up in a recursive loop due to the hook system.
+  defp notify_worker(offset, state) when offset > 0,
+    do: Informant.broadcast(state, {:clear, [[]]}, {:ok, offset})
+
+  defp notify_worker(_offset, _state),
+    do: :ok
+end
diff --git a/lib/cachex/actions/reset.ex b/lib/cachex/actions/reset.ex
index 268f0c7..54a5531 100644
--- a/lib/cachex/actions/reset.ex
+++ b/lib/cachex/actions/reset.ex
@@ -86,8 +86,8 @@ defmodule Cachex.Actions.Reset do
   #
   # This is just sugar around set membership whilst unpacking a hook record,
   # used in Enum iterations to avoid inlining functions for readability.
-  defp should_reset?(hook(module: module), hook_set),
-    do: module in hook_set
+  defp should_reset?(hook(module: module), hooks),
+    do: module in hooks
 
   # Notifies a hook of a reset.
   #
@@ -95,6 +95,6 @@ defmodule Cachex.Actions.Reset do
   # message to signal that the hook needs to reinitialize. Hooks have a
   # listener built into the server implementatnion in order to handle this
   # automatically, so there's nothing more we need to do.
-  defp notify_reset(hook(state: state, name: name)),
-    do: send(name, {:cachex_reset, state})
+  defp notify_reset(hook(args: args, name: name)),
+    do: send(name, {:cachex_reset, args})
 end
diff --git a/lib/cachex/actions/stats.ex b/lib/cachex/actions/stats.ex
index 02a8b0b..4ca2df4 100644
--- a/lib/cachex/actions/stats.ex
+++ b/lib/cachex/actions/stats.ex
@@ -23,7 +23,7 @@ defmodule Cachex.Actions.Stats do
   @spec execute(Cachex.t(), Keyword.t()) ::
           {:ok, %{}} | {:error, :stats_disabled}
   def execute(cache() = cache, _options) do
-    with {:ok, stats} <- Stats.retrieve(cache) do
+    with {:ok, stats} <- Stats.for_cache(cache) do
       hits_count = Map.get(stats, :hits, 0)
       miss_count = Map.get(stats, :misses, 0)
 
diff --git a/lib/cachex/actions/stream.ex b/lib/cachex/actions/stream.ex
index e01481f..4a6d707 100644
--- a/lib/cachex/actions/stream.ex
+++ b/lib/cachex/actions/stream.ex
@@ -57,23 +57,28 @@ defmodule Cachex.Actions.Stream do
       fn
         # we're finished!
         :"$end_of_table" ->
-          {:halt, nil}
+          handle_continue(:"$end_of_table")
 
         # we're starting!
         :"$start_of_table" ->
-          case :ets.select(name, spec, batch) do
-            :"$end_of_table" ->
-              {:halt, nil}
-
-            continuation ->
-              continuation
-          end
+          name
+          |> :ets.select(spec, batch)
+          |> handle_continue()
 
         # we're continuing!
         continuation ->
-          :ets.select(continuation)
+          continuation
+          |> :ets.select()
+          |> handle_continue()
       end,
       & &1
     )
   end
+
+  # Handles a continuation from ETS to terminate a stream.
+  defp handle_continue(:"$end_of_table"),
+    do: {:halt, nil}
+
+  defp handle_continue(continuation),
+    do: continuation
 end
diff --git a/lib/cachex/hook.ex b/lib/cachex/hook.ex
index fe16b25..907e844 100644
--- a/lib/cachex/hook.ex
+++ b/lib/cachex/hook.ex
@@ -8,6 +8,7 @@ defmodule Cachex.Hook do
   to execute either before or after the Cachex command, and can be blocking as
   needed.
   """
+  import Cachex.Spec
 
   #############
   # Behaviour #
@@ -151,4 +152,36 @@ defmodule Cachex.Hook do
       end
     end
   end
+
+  ##############
+  # Public API #
+  ##############
+
+  @doc """
+  Locates a hook for a cache.
+  """
+  @spec locate(Cachex.t(), module :: atom(), type :: :all | :pre | :post) ::
+          Cachex.Spec.hook() | nil
+  def locate(cache, module, type \\ :all)
+
+  def locate(cache, module, :all) do
+    case locate(cache, module, :post) do
+      nil -> locate(cache, module, :pre)
+      val -> val
+    end
+  end
+
+  def locate(cache(hooks: hooks(pre: hooks)), module, :pre),
+    do: locate_hook(hooks, module)
+
+  def locate(cache(hooks: hooks(post: hooks)), module, :post),
+    do: locate_hook(hooks, module)
+
+  ###############
+  # Private API #
+  ###############
+
+  # Find a hook based on module name.
+  defp locate_hook(hooks, module),
+    do: Enum.find(hooks, &match?(hook(module: ^module), &1))
 end
diff --git a/lib/cachex/limit/accessed.ex b/lib/cachex/limit/accessed.ex
new file mode 100644
index 0000000..b095721
--- /dev/null
+++ b/lib/cachex/limit/accessed.ex
@@ -0,0 +1,67 @@
+defmodule Cachex.Limit.Accessed do
+  @moduledoc """
+  Access based touch tracking for LRW pruning.
+
+  This module can be used to adapt `Cachex.prune/3` for LRU purposes instead of
+  the typical LRW. This hook will update the modification time of a cache entry
+  upon access by a read operation. This is a very basic way to provide LRU policies,
+  but it should suffice for most cases.
+
+  At the time of writing modification times are *not* updated when executing
+  commands on multiple keys, such as `Cachex.keys/2` and `Cachex.stream/3`, for
+  performance reasons. Again, this may change in future if necessary.
+  """
+  use Cachex.Hook
+
+  # touched actions
+  @actions [
+    :decr,
+    :exists?,
+    :fetch,
+    :get,
+    :incr,
+    :invoke,
+    :ttl,
+    :update
+  ]
+
+  ##################
+  # Hook Behaviour #
+  ##################
+
+  @doc """
+  Returns the actions this hook should listen on.
+  """
+  @spec actions :: [atom]
+  def actions,
+    do: @actions
+
+  @doc """
+  Returns the provisions this hook requires.
+  """
+  @spec provisions :: [atom]
+  def provisions,
+    do: [:cache]
+
+  ####################
+  # Server Callbacks #
+  ####################
+
+  @doc false
+  # Handles notification of a cache action.
+  #
+  # This will update the modification time of a key if tracked in a successful cache
+  # action. In combination with LRW caching, this provides a simple LRU policy.
+  def handle_notify({_action, [key | _]}, _result, cache) do
+    {:ok, true} = Cachex.touch(cache, key)
+    {:ok, cache}
+  end
+
+  @doc false
+  # Receives a provisioned cache instance.
+  #
+  # The provided cache is then stored in the cache and used for cache calls going
+  # forwards, in order to skip the lookups inside the cache overseer for performance.
+  def handle_provision({:cache, cache}, _cache),
+    do: {:ok, cache}
+end
diff --git a/lib/cachex/policy/lrw/evented.ex b/lib/cachex/limit/evented.ex
similarity index 79%
rename from lib/cachex/policy/lrw/evented.ex
rename to lib/cachex/limit/evented.ex
index 73f81f0..28f8f54 100644
--- a/lib/cachex/policy/lrw/evented.ex
+++ b/lib/cachex/limit/evented.ex
@@ -1,4 +1,4 @@
-defmodule Cachex.Policy.LRW.Evented do
+defmodule Cachex.Limit.Evented do
   @moduledoc """
   Evented least recently written eviction policy for Cachex.
 
@@ -7,14 +7,16 @@ defmodule Cachex.Policy.LRW.Evented do
   way. This policy enforces cache bounds and limits far more accurately than other
   scheduled implementations, but comes at a higher memory cost (due to the message
   passing between hooks).
-  """
-  use Cachex.Hook
 
-  # import macros
-  import Cachex.Spec
+  ## Initialization
+
+      hook(module: Cachex.Limit.Evented, args: {
+        500,  # setting cache max size
+        []    # options for `Cachex.prune/3`
+      })
 
-  # add internal aliases
-  alias Cachex.Policy.LRW
+  """
+  use Cachex.Hook
 
   # actions which didn't trigger
   @ignored [:error, :ignore]
@@ -51,8 +53,8 @@ defmodule Cachex.Policy.LRW.Evented do
 
   @doc false
   # Initializes this policy using the limit being enforced.
-  def init(limit() = limit),
-    do: {:ok, {nil, limit}}
+  def init(args),
+    do: {:ok, {nil, args}}
 
   @doc false
   # Handles notification of a cache action.
@@ -62,9 +64,9 @@ defmodule Cachex.Policy.LRW.Evented do
   #
   # Note that this will ignore error results and only operates on actions which are
   # able to cause a net gain in cache size (so removals are also ignored).
-  def handle_notify(_message, {status, _value}, {cache, limit} = opts)
+  def handle_notify(_message, {status, _value}, {cache, {size, options}} = opts)
       when status not in @ignored do
-    LRW.apply_limit(cache, limit)
+    {:ok, true} = Cachex.prune(cache, size, options)
     {:ok, opts}
   end
 
@@ -76,6 +78,6 @@ defmodule Cachex.Policy.LRW.Evented do
   #
   # The provided cache is then stored in the cache and used for cache calls going
   # forwards, in order to skip the lookups inside the cache overseer for performance.
-  def handle_provision({:cache, cache}, {_cache, limit}),
-    do: {:ok, {cache, limit}}
+  def handle_provision({:cache, cache}, {_cache, options}),
+    do: {:ok, {cache, options}}
 end
diff --git a/lib/cachex/policy/lrw/scheduled.ex b/lib/cachex/limit/scheduled.ex
similarity index 63%
rename from lib/cachex/policy/lrw/scheduled.ex
rename to lib/cachex/limit/scheduled.ex
index 2672219..4890f2e 100644
--- a/lib/cachex/policy/lrw/scheduled.ex
+++ b/lib/cachex/limit/scheduled.ex
@@ -1,4 +1,4 @@
-defmodule Cachex.Policy.LRW.Scheduled do
+defmodule Cachex.Limit.Scheduled do
   @moduledoc """
   Scheduled least recently written eviction policy for Cachex.
 
@@ -7,14 +7,25 @@ defmodule Cachex.Policy.LRW.Scheduled do
   accuracy as `Cachex.Policy.LRW.Evented`, but has potential for some delay. The
   main advantage of this implementation is a far lower memory cost due to not
   using hook messages.
-  """
-  use Cachex.Hook
 
-  # import macros
-  import Cachex.Spec
+  ## Initialization
+
+      hook(module: Cachex.Limit.Scheduled, args: {
+        500,  # setting cache max size
+        [],   # options for `Cachex.prune/3`
+        []    # options for `Cachex.Limit.Scheduled`
+      })
+
+  ## Options
+
+    * `:frequency`
 
-  # add internal aliases
-  alias Cachex.Policy.LRW
+      The polling frequency for this hook to use when scheduling cache pruning.
+      This should be an non-negative number of milliseconds. Defaults to `1000`,
+      which is once per second.
+
+  """
+  use Cachex.Hook
 
   ######################
   # Hook Configuration #
@@ -40,19 +51,16 @@ defmodule Cachex.Policy.LRW.Scheduled do
 
   @doc false
   # Initializes this policy using the limit being enforced.
-  def init(limit() = limit),
-    do: {schedule(limit), {nil, limit}}
+  def init({_size, _options, scheduling} = args),
+    do: {schedule(scheduling), {nil, args}}
 
   @doc false
   # Handles notification of a cache action.
   #
   # This will execute a bounds check on a cache and schedule a new check.
-  def handle_info(:policy_check, {cache, limit} = opts) do
-    unless is_nil(cache) do
-      LRW.apply_limit(cache, limit)
-    end
-
-    schedule(limit) && {:noreply, opts}
+  def handle_info(:policy_check, {cache, {size, options, scheduling}} = args) do
+    {:ok, true} = Cachex.prune(cache, size, options)
+    schedule(scheduling) && {:noreply, args}
   end
 
   @doc false
@@ -60,15 +68,15 @@ defmodule Cachex.Policy.LRW.Scheduled do
   #
   # The provided cache is then stored in the state and used for cache calls going
   # forwards, in order to skip the lookups inside the cache overseer for performance.
-  def handle_provision({:cache, cache}, {_cache, limit}),
-    do: {:ok, {cache, limit}}
+  def handle_provision({:cache, cache}, {_cache, args}),
+    do: {:ok, {cache, args}}
 
   ###############
   # Private API #
   ###############
 
   # Schedules a check to occur after the designated interval.
-  defp schedule(limit(options: options)) do
+  defp schedule(options) do
     options
     |> Keyword.get(:frequency, 1000)
     |> :erlang.send_after(self(), :policy_check)
diff --git a/lib/cachex/options.ex b/lib/cachex/options.ex
index f6d371e..2963adc 100644
--- a/lib/cachex/options.ex
+++ b/lib/cachex/options.ex
@@ -17,7 +17,6 @@ defmodule Cachex.Options do
   # option parser order
   @option_parsers [
     :name,
-    :limit,
     :hooks,
     :router,
     :ordered,
@@ -196,37 +195,11 @@ defmodule Cachex.Options do
   #
   # In addition to the hooks already provided, this will also deal with the
   # notion of statistics hooks and limits, as they can both define hooks.
-  defp parse_type(:hooks, cache(name: name, limit: limit) = cache, options) do
+  defp parse_type(:hooks, cache() = cache, options) do
     hooks =
-      Enum.concat([
-        # stats hook generation
-        case !!options[:stats] do
-          false ->
-            []
-
-          true ->
-            [
-              hook(
-                module: Cachex.Stats,
-                name: name(name, :stats)
-              )
-            ]
-        end,
-
-        # limit hook generation
-        case limit do
-          limit(size: nil) ->
-            []
-
-          limit(policy: policy) ->
-            apply(policy, :hooks, [limit])
-        end,
-
-        # provided hooks lists
-        options
-        |> Keyword.get(:hooks, [])
-        |> List.wrap()
-      ])
+      options
+      |> Keyword.get(:hooks, [])
+      |> List.wrap()
 
     # validation and division into a hooks record
     case validated?(hooks, :hook) do
@@ -243,24 +216,6 @@ defmodule Cachex.Options do
     end
   end
 
-  # Sets up any provided limit structures.
-  #
-  # This will allow shorthanding of a numeric value to act as a size
-  # to bound the cache to. This will provide defaults for all other
-  # fields in the limit structure.
-  defp parse_type(:limit, cache, options) do
-    limit =
-      case Keyword.get(options, :limit) do
-        limit() = limit -> limit
-        size -> limit(size: size)
-      end
-
-    case Validator.valid?(:limit, limit) do
-      false -> error(:invalid_limit)
-      true -> cache(cache, limit: limit)
-    end
-  end
-
   # Creates a base cache record from a cache name.
   #
   # This is separated out just to allow being part of the parse pipeline
diff --git a/lib/cachex/policy.ex b/lib/cachex/policy.ex
deleted file mode 100644
index 9f844ba..0000000
--- a/lib/cachex/policy.ex
+++ /dev/null
@@ -1,38 +0,0 @@
-defmodule Cachex.Policy do
-  @moduledoc """
-  Module controlling policy behaviour definitions.
-
-  This module purely exposes the behaviour and convenience macros for
-  creating a custom policy. It's used internally be `Cachex.Policy.LRW`
-  and provides very little more than an interface to adhere to.
-  """
-  import Cachex.Spec
-
-  #############
-  # Behaviour #
-  #############
-
-  @doc """
-  Returns any hook definitions required for this policy.
-  """
-  @callback hooks(Cachex.Spec.limit()) :: [Cachex.Spec.hook()]
-
-  ##################
-  # Implementation #
-  ##################
-
-  @doc false
-  defmacro __using__(_) do
-    quote location: :keep do
-      # include the behaviour
-      @behaviour Cachex.Policy
-
-      @doc false
-      def hooks(_limit),
-        do: []
-
-      # all can be overridden
-      defoverridable hooks: 1
-    end
-  end
-end
diff --git a/lib/cachex/policy/lru.ex b/lib/cachex/policy/lru.ex
deleted file mode 100644
index b761a63..0000000
--- a/lib/cachex/policy/lru.ex
+++ /dev/null
@@ -1,98 +0,0 @@
-defmodule Cachex.Policy.LRU do
-  @moduledoc """
-  Least recently used eviction policies for Cachex.
-
-  This module provides basic bindings around `Cachex.Policy.LRW` to adapt the
-  LRW caching policies for LRU purposes. As such, please see the documentation
-  of `Cachex.Policy.LRW` for a full list of supported options.
-
-  Adding LRU support is done by attaching an additional hook to enable updates
-  of the modification time in a cache entry on read. This is a very basic way
-  to provide LRU policies, but it should suffice for most cases. It's possible
-  that this may change in future, so this implementation should not be relied upon.
-
-  At the time of writing modification times are *not* updated when executing
-  commands on multiple keys, such as `Cachex.keys/2` and `Cachex.stream/3`, for
-  performance reasons. Again, this may change in future if necessary.
-  """
-  use Cachex.Hook
-  use Cachex.Policy
-
-  # import macros
-  import Cachex.Spec
-
-  # touched actions
-  @actions [
-    :decr,
-    :exists?,
-    :fetch,
-    :get,
-    :incr,
-    :invoke,
-    :ttl,
-    :update
-  ]
-
-  # actions which didn't trigger
-  @ignored [:error, :ignore]
-
-  ####################
-  # Policy Behaviour #
-  ####################
-
-  @doc """
-  Configures hooks required to back this policy.
-  """
-  def hooks(limit() = limit),
-    do: [
-      hook(
-        state: nil,
-        module: __MODULE__
-      )
-      | Cachex.Policy.LRW.hooks(limit)
-    ]
-
-  ##################
-  # Hook Behaviour #
-  ##################
-
-  @doc """
-  Returns the actions this policy should listen on.
-  """
-  @spec actions :: [atom]
-  def actions,
-    do: @actions
-
-  @doc """
-  Returns the provisions this policy requires.
-  """
-  @spec provisions :: [atom]
-  def provisions,
-    do: [:cache]
-
-  ####################
-  # Server Callbacks #
-  ####################
-
-  @doc false
-  # Handles notification of a cache action.
-  #
-  # This will update the modification time of a key if tracked in a successful cache
-  # action. In combination with LRW caching, this provides a simple LRU policy.
-  def handle_notify({_action, [key | _]}, {status, _value}, cache)
-      when status not in @ignored do
-    {:ok, true} = Cachex.touch(cache, key, const(:notify_false))
-    {:ok, cache}
-  end
-
-  def handle_notify(_event, _result, cache),
-    do: {:ok, cache}
-
-  @doc false
-  # Receives a provisioned cache instance.
-  #
-  # The provided cache is then stored in the cache and used for cache calls going
-  # forwards, in order to skip the lookups inside the cache overseer for performance.
-  def handle_provision({:cache, cache}, _cache),
-    do: {:ok, cache}
-end
diff --git a/lib/cachex/policy/lrw.ex b/lib/cachex/policy/lrw.ex
deleted file mode 100644
index 00abc42..0000000
--- a/lib/cachex/policy/lrw.ex
+++ /dev/null
@@ -1,169 +0,0 @@
-defmodule Cachex.Policy.LRW do
-  @moduledoc """
-  Least recently written eviction policies for Cachex.
-
-  This module provides general utilities for implementing an eviction policy for
-  Cachex which will evict the least-recently written entries from the cache. This
-  is determined by the modified time inside each cache record, which means that we
-  don't have to store any additional tables to keep track of access time.
-
-  There are several options recognised by this policy which can be passed inside the
-  limit structure when configuring your cache at startup:
-
-    * `:batch_size`
-
-      The batch size to use when paginating the cache to evict records. This defaults
-      to 100, which is typically going to be fine for most cases, but this option is
-      exposed in case there is need to customize it.
-
-    * `:frequency`
-
-      When this policy operates in scheduled mode, this option controls the frequency
-      with which bounds will be checked. This is specified in milliseconds, and will
-      default to once per second (1000). Feel free to tune this based on how strictly
-      you wish to enforce your cache limits.
-
-    * `:immediate`
-
-      Sets this policy to enforce bounds reactively. If this option is set to `true`,
-      bounds will be checked immediately when a write is made to the cache rather than
-      on a timed schedule. This has the result of being much more accurate with the
-      size of a cache, but has higher overhead due to listening on cache writes.
-
-      Setting this to `true` will disable the scheduled checks and thus the `:frequency`
-      option is ignored in this case.
-
-  While the overall behaviour of this policy should always result in the same outcome,
-  the way it operates internally may change. As such, the internals of this module
-  should not be relied upon and should not be considered part of the public API.
-  """
-  use Cachex.Policy
-
-  # import macros
-  import Cachex.Spec
-
-  # add internal aliases
-  alias Cachex.Query
-  alias Cachex.Services.Informant
-
-  # compile our match to avoid recalculating
-  @ets_match Query.build(output: {:key, :modified})
-
-  ####################
-  # Policy Behaviour #
-  ####################
-
-  @doc """
-  Configures hooks required to back this policy.
-  """
-  def hooks(limit(options: options) = limit),
-    do: [
-      hook(
-        state: limit,
-        module:
-          case Keyword.get(options, :immediate) do
-            true -> __MODULE__.Evented
-            _not -> __MODULE__.Scheduled
-          end
-      )
-    ]
-
-  #############
-  # Algorithm #
-  #############
-
-  @doc false
-  # Enforces cache bounds based on the provided limit.
-  #
-  # This function will enforce cache bounds using a least recently written (LRW)
-  # eviction policy. It will trigger a Janitor purge to clear expired records
-  # before attempting to trim older cache entries.
-  #
-  # Please see module documentation for options available inside the limits.
-  @spec apply_limit(Cachex.t(), Cachex.Spec.limit()) :: :ok
-  def apply_limit(cache() = cache, limit() = limit) do
-    limit(size: max_size, reclaim: reclaim, options: options) = limit
-
-    batch_size =
-      case Keyword.get(options, :batch_size, 100) do
-        val when val < 0 -> 100
-        val -> val
-      end
-
-    reclaim_bound = round(max_size * reclaim)
-
-    case Cachex.size!(cache, const(:notify_false)) do
-      cache_size when cache_size <= max_size ->
-        notify_worker(0, cache)
-
-      cache_size ->
-        cache_size
-        |> calculate_reclaim(max_size, reclaim_bound)
-        |> calculate_poffset(cache)
-        |> erase_lower_bound(cache, batch_size)
-        |> notify_worker(cache)
-    end
-  end
-
-  # Calculates the space to reclaim inside a cache.
-  #
-  # This is a function of the maximum cache size, the reclaim bound and the
-  # current size of the cache. A positive result from this function means that
-  # we need to carry out evictions, whereas a negative results means that the
-  # cache is currently underpopulated.
-  defp calculate_reclaim(current_size, max_size, reclaim_bound),
-    do: (max_size - reclaim_bound - current_size) * -1
-
-  # Calculates the purge offset of a cache.
-  #
-  # Basically this means that if the cache is overpopulated, we would trigger
-  # a Janitor purge to see if expirations bring the cache back under the max
-  # size limits. The resulting amount of removed records is then offset against
-  # the reclaim space, meaning that a positive result require us to carry out
-  # further evictions manually down the chain.
-  defp calculate_poffset(reclaim_space, cache) when reclaim_space > 0,
-    do: reclaim_space - Cachex.purge!(cache)
-
-  # Erases the least recently written records up to the offset limit.
-  #
-  # If the provided offset is not positive we don't do anything as it signals that
-  # the cache is already within the correctly sized limits so we just pass through
-  # as a no-op.
-  #
-  # In the case the offset is positive, it represents the number of entries we need
-  # to remove from the cache table. We do this by traversing the underlying ETS table,
-  # which only selects the key and touch time as a minor optimization. The key is
-  # naturally required when it comes to removing the document, and the touch time is
-  # used to determine the sort order required for LRW.
-  defp erase_lower_bound(offset, cache(name: name) = cache, batch)
-       when offset > 0 do
-    cache
-    |> Cachex.stream!(@ets_match, const(:notify_false) ++ [batch_size: batch])
-    |> Enum.sort(fn {_k1, t1}, {_k2, t2} -> t1 < t2 end)
-    |> Enum.take(offset)
-    |> Enum.each(fn {k, _t} -> :ets.delete(name, k) end)
-
-    offset
-  end
-
-  defp erase_lower_bound(offset, _state, _batch),
-    do: offset
-
-  # Broadcasts the number of removed entries to the cache hooks.
-  #
-  # If the offset is not positive we didn't have to remove anything and so we
-  # don't broadcast any results. An 0 Tuple is returned just to keep compatibility
-  # with the response type from `Informant.broadcast/3`.
-  #
-  # It should be noted that we use a `:clear` action here as these evictions are
-  # based on size and not on expiration. The evictions done during the purge earlier
-  # in the pipeline are reported separately and we're only reporting the delta at this
-  # point in time. Therefore remember that it's important that we're ignoring the
-  # results of `clear()` and `purge()` calls in this hook, otherwise we would end
-  # up in a recursive loop due to the hook system.
-  defp notify_worker(offset, state) when offset > 0,
-    do: Informant.broadcast(state, {:clear, [[]]}, {:ok, offset})
-
-  defp notify_worker(_offset, _state),
-    do: :ok
-end
diff --git a/lib/cachex/router.ex b/lib/cachex/router.ex
index 49b2574..5b2785d 100644
--- a/lib/cachex/router.ex
+++ b/lib/cachex/router.ex
@@ -148,19 +148,15 @@ defmodule Cachex.Router do
   defp result_merge(left, right) when is_boolean(left),
     do: left && right
 
+  # coveralls-ignore-start
   defp result_merge(left, right) when is_map(left) do
-    Map.merge(left, right, fn
-      :creation_date, _left, right ->
-        right
-
-      key, left, right when key in [:hit_rate, :miss_rate] ->
-        (left + right) / 2
-
-      _key, left, right ->
-        result_merge(left, right)
+    Map.merge(left, right, fn _, left, right ->
+      result_merge(left, right)
     end)
   end
 
+  # coveralls-ignore-stop
+
   # Provides handling for local actions on this node.
   #
   # This will provide handling of notifications across hooks before and after
@@ -189,7 +185,7 @@ defmodule Cachex.Router do
       Informant.broadcast(
         cache,
         message,
-        Keyword.get(option, :hook_result, result)
+        Keyword.get(option, :result, result)
       )
     end
 
@@ -236,8 +232,7 @@ defmodule Cachex.Router do
     :keys,
     :purge,
     :reset,
-    :size,
-    :stats
+    :size
   ]
 
   # Provides handling of cross-node actions distributed over remote nodes.
@@ -293,7 +288,14 @@ defmodule Cachex.Router do
   end
 
   # actions which always run locally
-  @local_actions [:inspect, :restore, :save, :warm]
+  @local_actions [
+    :inspect,
+    :prune,
+    :restore,
+    :save,
+    :stats,
+    :warm
+  ]
 
   # Provides handling of `:inspect` operations.
   #
@@ -311,12 +313,11 @@ defmodule Cachex.Router do
   # Provides handling of `:transaction` operations.
   #
   # These operations can only execute if their keys slot to the same remote nodes.
-  defp route_cluster(cache, module, {:transaction, [keys | _]} = call) do
-    case keys do
-      [] -> route_local(cache, module, call)
-      _ -> route_batch(cache, module, call, & &1)
-    end
-  end
+  defp route_cluster(cache, module, {:transaction, [[] | _]} = call),
+    do: route_local(cache, module, call)
+
+  defp route_cluster(cache, module, {:transaction, [_keys | _]} = call),
+    do: route_batch(cache, module, call, & &1)
 
   # Any other actions are explicitly disabled in distributed environments.
   defp route_cluster(_cache, _module, _call),
diff --git a/lib/cachex/services/informant.ex b/lib/cachex/services/informant.ex
index 3c0dd49..2760bd1 100644
--- a/lib/cachex/services/informant.ex
+++ b/lib/cachex/services/informant.ex
@@ -94,11 +94,11 @@ defmodule Cachex.Services.Informant do
   end
 
   # Generates a Supervisor specification for a hook.
-  defp spec(hook(module: module, name: name, state: state)) do
+  defp spec(hook(module: module, name: name, args: args)) do
     options =
       case name do
-        nil -> [module, state]
-        val -> [module, state, [name: val]]
+        nil -> [module, args]
+        val -> [module, args, [name: val]]
       end
 
     %{id: module, start: {GenServer, :start_link, options}}
diff --git a/lib/cachex/services/overseer.ex b/lib/cachex/services/overseer.ex
index c1d6f15..ab61f65 100644
--- a/lib/cachex/services/overseer.ex
+++ b/lib/cachex/services/overseer.ex
@@ -59,7 +59,7 @@ defmodule Cachex.Services.Overseer do
   Retrieving a cache will map the provided argument to a
   cache record if available, otherwise a nil value.
   """
-  @spec get(atom | Cachex.t()) :: Cachex.t() | nil
+  @spec get(Cachex.t()) :: Cachex.t() | nil
   def get(cache() = cache),
     do: cache
 
diff --git a/lib/cachex/spec.ex b/lib/cachex/spec.ex
index eb695b6..752e38a 100644
--- a/lib/cachex/spec.ex
+++ b/lib/cachex/spec.ex
@@ -39,7 +39,6 @@ defmodule Cachex.Spec do
             compressed: boolean,
             expiration: expiration,
             hooks: hooks,
-            limit: limit,
             ordered: boolean,
             router: router,
             transactions: boolean,
@@ -77,7 +76,7 @@ defmodule Cachex.Spec do
   @type hook ::
           record(:hook,
             module: atom,
-            state: any,
+            args: Keyword.t(),
             name: GenServer.server()
           )
 
@@ -88,15 +87,6 @@ defmodule Cachex.Spec do
             post: [hook]
           )
 
-  # Record specification for a cache limit
-  @type limit ::
-          record(:limit,
-            size: integer,
-            policy: atom,
-            reclaim: number,
-            options: Keyword.t()
-          )
-
   # Record specification for a router instance
   @type router ::
           record(:router,
@@ -212,7 +202,7 @@ defmodule Cachex.Spec do
   """
   defrecord :hook,
     module: nil,
-    state: nil,
+    args: nil,
     name: nil
 
   @doc """
@@ -225,25 +215,6 @@ defmodule Cachex.Spec do
     pre: [],
     post: []
 
-  @doc """
-  Creates a limit record from the provided values.
-
-  A limit record represents size bounds on a cache, and the way size should be reclaimed.
-
-  A limit should have a valid integer as the maximum cache size, which is used to determine
-  when to cull records. By default, an LRW style policy will be applied to remove old records
-  but this can also be customized using the policy value. The amount of space to reclaim at
-  once can be provided using the reclaim option.
-
-  You can also specify options to pass through to the policy server, in order to customize
-  policy behaviour.
-  """
-  defrecord :limit,
-    size: nil,
-    policy: Cachex.Policy.LRW,
-    reclaim: 0.1,
-    options: []
-
   @doc """
   Creates a router record from the provided values.
 
@@ -315,12 +286,6 @@ defmodule Cachex.Spec do
   @spec hooks(hooks, Keyword.t()) :: hooks
   defmacro hooks(record, args)
 
-  @doc """
-  Updates a limit record from the provided values.
-  """
-  @spec limit(limit, Keyword.t()) :: limit
-  defmacro limit(record, args)
-
   @doc """
   Updates a router record from the provided values.
   """
@@ -371,7 +336,7 @@ defmodule Cachex.Spec do
       quote(
         do: [
           via: const(:purge_override_call),
-          hook_result: const(:purge_override_result)
+          result: const(:purge_override_result)
         ]
       )
 
diff --git a/lib/cachex/spec/validator.ex b/lib/cachex/spec/validator.ex
index 82438a6..e271497 100644
--- a/lib/cachex/spec/validator.ex
+++ b/lib/cachex/spec/validator.ex
@@ -17,7 +17,6 @@ defmodule Cachex.Spec.Validator do
           | Cachex.Spec.expiration()
           | Cachex.Spec.hook()
           | Cachex.Spec.hooks()
-          | Cachex.Spec.limit()
           | Cachex.Spec.warmer()
 
   ##############
@@ -105,24 +104,6 @@ defmodule Cachex.Spec.Validator do
     check3
   end
 
-  # Validates a limit specification record.
-  #
-  # This has to validate all fields in the record, with the size being a nillable integer,
-  # the policy being a valid module, the reclaim space being a valid float between 0 and 1,
-  # and a valid keyword list as the options.
-  def valid?(:limit, limit() = limit) do
-    limit(size: size, policy: policy, reclaim: reclaim, options: options) =
-      limit
-
-    check1 = module?(policy)
-    check2 = check1 and nillable?(size, &is_positive_integer/1)
-    check3 = check2 and is_number(reclaim)
-    check4 = check3 and reclaim > 0
-    check5 = check4 and reclaim <= 1
-    check6 = check5 and Keyword.keyword?(options)
-    check6
-  end
-
   # Validates a router specification record.
   #
   # This will validate the correctly implemented `Cachex.Router` behaviour
@@ -175,14 +156,6 @@ defmodule Cachex.Spec.Validator do
   defp enum?(enum, condition),
     do: unsafe?(fn -> Enum.all?(enum, condition) end)
 
-  @doc false
-  # Determines if the provided value is a valid module.
-  #
-  # This is done by attempting to retrieve the module info using
-  # the __info__/1 macro only available to valid Elixir modules.
-  defp module?(module),
-    do: unsafe?(fn -> !!module.__info__(:module) end)
-
   @doc false
   # Determines if a condition is truthy.
   #
diff --git a/lib/cachex/stats.ex b/lib/cachex/stats.ex
index 6eaf824..b08c0e8 100644
--- a/lib/cachex/stats.ex
+++ b/lib/cachex/stats.ex
@@ -11,6 +11,7 @@ defmodule Cachex.Stats do
   can also be mocked easily using raw server calls to `handle_notify/3`.
   """
   use Cachex.Hook
+  alias Cachex.Hook
 
   # need our macros
   import Cachex.Spec
@@ -29,37 +30,17 @@ defmodule Cachex.Stats do
     :update
   ]
 
-  ##############
-  # Public API #
-  ##############
-
-  @doc """
-  Determines if stats are enabled for a cache.
-  """
-  @spec enabled?(Cachex.t()) :: boolean
-  def enabled?(cache() = cache),
-    do: locate(cache) != nil
-
-  @doc """
-  Locates a stats hook for a cache, if enabled.
-  """
-  @spec locate(Cachex.t()) :: Cachex.Spec.hook() | nil
-  def locate(cache(hooks: hooks(post: post_hooks))),
-    do: Enum.find(post_hooks, &match?(hook(module: Cachex.Stats), &1))
-
   @doc """
   Retrieves the latest statistics for a cache.
   """
-  @spec retrieve(Cachex.t()) :: {:ok, map()} | {:error, atom()}
-  def retrieve(cache(name: name) = cache) do
-    case enabled?(cache) do
-      false ->
+  @spec for_cache(Cachex.t()) :: {:ok, map()} | {:error, atom()}
+  def for_cache(cache() = cache) do
+    case Hook.locate(cache, __MODULE__) do
+      nil ->
         error(:stats_disabled)
 
-      true ->
-        name
-        |> name(:stats)
-        |> GenServer.call(:retrieve)
+      hook(name: name) ->
+        GenServer.call(name, :retrieve)
     end
   end
 
diff --git a/mix.exs b/mix.exs
index 106f395..d449dc8 100644
--- a/mix.exs
+++ b/mix.exs
@@ -39,6 +39,7 @@ defmodule Cachex.Mixfile do
           "docs/general/streaming-records.md",
           "docs/management/limiting-caches.md",
           "docs/management/expiring-records.md",
+          "docs/management/stats-gathering.md",
           "docs/migrations/migrating-to-v3.md",
           "docs/migrations/migrating-to-v2.md",
           "docs/routing/distributed-caches.md",


Test patch:
diff --git a/test/cachex/actions/prune_test.exs b/test/cachex/actions/prune_test.exs
new file mode 100644
index 0000000..0810552
--- /dev/null
+++ b/test/cachex/actions/prune_test.exs
@@ -0,0 +1,109 @@
+defmodule Cachex.Actions.PruneTest do
+  use Cachex.Test.Case
+
+  test "pruning a cache to a size" do
+    # create a new test cache
+    cache = TestUtils.create_cache()
+
+    # insert 100 keys
+    for i <- 1..100 do
+      Cachex.put!(cache, i, i)
+    end
+
+    # guarantee we have 100 keys in the cache
+    assert Cachex.size(cache) == {:ok, 100}
+
+    # trigger a pruning down to 50 keys
+    assert Cachex.prune(cache, 50) == {:ok, true}
+
+    # verify that we're down to 50 keys
+    assert Cachex.size(cache) == {:ok, 45}
+  end
+
+  test "pruning a cache to a size with a custom reclaim" do
+    # create a new test cache
+    cache = TestUtils.create_cache()
+
+    # insert 100 keys
+    for i <- 1..100 do
+      Cachex.put!(cache, i, i)
+    end
+
+    # guarantee we have 100 keys in the cache
+    assert Cachex.size(cache) == {:ok, 100}
+
+    # trigger a pruning down to 50 keys, reclaiming 10%
+    assert Cachex.prune(cache, 50, reclaim: 0) == {:ok, true}
+
+    # verify that we're down to 50 keys
+    assert Cachex.size(cache) == {:ok, 50}
+  end
+
+  # This test ensures that the cache eviction policy will evict any expired values
+  # before removing the oldest. This is to make sure that we don't remove anything
+  # without good reason. To verify this we add 50 keys with a TTL more recently
+  # than those without and cross the cache limit. We then validate that all expired
+  # keys have been purged, and no other keys have been removed as the purge takes
+  # the cache size back under the maximum size.
+  test "evicting by removing expired keys" do
+    # create a new test cache
+    cache = TestUtils.create_cache()
+
+    # retrieve the cache state
+    state = Services.Overseer.retrieve(cache)
+
+    # set 50 keys without ttl
+    for x <- 1..50 do
+      # set the key
+      {:ok, true} = Cachex.put(state, x, x)
+
+      # tick to make sure each has a new touch time
+      :timer.sleep(1)
+    end
+
+    # set a more recent 50 keys
+    for x <- 51..100 do
+      # set the key
+      {:ok, true} = Cachex.put(state, x, x, expire: 1)
+
+      # tick to make sure each has a new touch time
+      :timer.sleep(1)
+    end
+
+    # retrieve the cache size
+    size1 = Cachex.size!(cache)
+
+    # verify the cache size
+    assert(size1 == 100)
+
+    # add a new key to the cache to trigger oversize
+    {:ok, true} = Cachex.put(state, 101, 101)
+
+    # trigger the cache pruning down to 100 records
+    {:ok, true} = Cachex.prune(cache, 100, reclaim: 0.3, batch_size: -1)
+
+    # verify the cache shrinks to 51%
+    assert Cachex.size(state) == {:ok, 51}
+
+    # our validation step
+    validate = fn range, expected ->
+      # iterate all keys in the range
+      for x <- range do
+        # retrieve whether the key exists
+        exists = Cachex."exists?!"(state, x)
+
+        # verify whether it exists
+        assert(exists == expected)
+      end
+    end
+
+    # verify the first 50 keys are retained
+    validate.(1..50, true)
+
+    # verify the second 50 are removed
+    validate.(51..100, false)
+
+    # verify the last key added is retained
+    validate.(101..101, true)
+  end
+end
diff --git a/test/cachex/actions/reset_test.exs b/test/cachex/actions/reset_test.exs
index 0548dcc..7683596 100644
--- a/test/cachex/actions/reset_test.exs
+++ b/test/cachex/actions/reset_test.exs
@@ -11,7 +11,12 @@ defmodule Cachex.Actions.ResetTest do
   # coverage tools, as the entire point is that it doesn't do anything.
   test "resetting a cache" do
     # create a test cache
-    cache1 = TestUtils.create_cache(stats: true)
+    cache1 =
+      TestUtils.create_cache(
+        hooks: [
+          hook(module: Cachex.Stats)
+        ]
+      )
 
     # create a cache with no hooks
     cache2 = TestUtils.create_cache()
@@ -64,7 +69,12 @@ defmodule Cachex.Actions.ResetTest do
   # to verify that the cache is empty after the reset.
   test "resetting only a cache" do
     # create a test cache
-    cache = TestUtils.create_cache(stats: true)
+    cache =
+      TestUtils.create_cache(
+        hooks: [
+          hook(module: Cachex.Stats)
+        ]
+      )
 
     # get current time
     ctime1 = now()
@@ -105,7 +115,12 @@ defmodule Cachex.Actions.ResetTest do
   # that the creation_date of the stats hook is reset properly.
   test "resetting only a cache's hooks" do
     # create a test cache
-    cache = TestUtils.create_cache(stats: true)
+    cache =
+      TestUtils.create_cache(
+        hooks: [
+          hook(module: Cachex.Stats)
+        ]
+      )
 
     # get current time
     ctime1 = now()
diff --git a/test/cachex/actions/stats_test.exs b/test/cachex/actions/stats_test.exs
index 0a7532e..75c4a5d 100644
--- a/test/cachex/actions/stats_test.exs
+++ b/test/cachex/actions/stats_test.exs
@@ -6,7 +6,12 @@ defmodule Cachex.Actions.StatsTest do
   # filtered by the provided flags in order to customize output correctly.
   test "retrieving stats for a cache" do
     # create a test cache
-    cache = TestUtils.create_cache(stats: true)
+    cache =
+      TestUtils.create_cache(
+        hooks: [
+          hook(module: Cachex.Stats)
+        ]
+      )
 
     # retrieve current time
     ctime = now()
@@ -48,11 +53,14 @@ defmodule Cachex.Actions.StatsTest do
   # values, to avoid arithmetic errors. We very 100% hit/miss rates, as well as
   # 50% either way.
   test "retrieving different rate combinations" do
+    # create a stats hook
+    hook = hook(module: Cachex.Stats)
+
     # create test caches
-    cache1 = TestUtils.create_cache(stats: true)
-    cache2 = TestUtils.create_cache(stats: true)
-    cache3 = TestUtils.create_cache(stats: true)
-    cache4 = TestUtils.create_cache(stats: true)
+    cache1 = TestUtils.create_cache(hooks: [hook])
+    cache2 = TestUtils.create_cache(hooks: [hook])
+    cache3 = TestUtils.create_cache(hooks: [hook])
+    cache4 = TestUtils.create_cache(hooks: [hook])
 
     # set cache1 to 100% misses
     {:ok, nil} = Cachex.get(cache1, 1)
@@ -143,40 +151,4 @@ defmodule Cachex.Actions.StatsTest do
       }
     )
   end
-
-  # This test verifies that the distributed router correctly controls
-  # the stats/2 action in such a way that it can clean both a local
-  # node as well as a remote node. We don't have to check functionality
-  # of the entire action; just the actual routing of the action to the
-  # target node(s) is of interest here.
-  @tag distributed: true
-  test "retrieving stats for a cache cluster" do
-    # create a new cache cluster for cleaning
-    {cache, _nodes, _cluster} = TestUtils.create_cache_cluster(2, stats: true)
-
-    # we know that 1 & 2 hash to different nodes
-    {:ok, true} = Cachex.put(cache, 1, 1)
-    {:ok, true} = Cachex.put(cache, 2, 2)
-
-    # generate a hit rate to check that
-    {:ok, 1} = Cachex.get(cache, 1)
-    {:ok, nil} = Cachex.get(cache, 3)
-
-    # retrieve the stats from both local & remote
-    {:ok, stats1} = Cachex.stats(cache, local: true)
-    {:ok, stats2} = Cachex.stats(cache, local: false)
-
-    # check 2 local, 5 global
-    assert(stats1.calls.put == 1)
-    assert(stats1.operations == 2)
-
-    assert(
-      stats1.hit_rate == 100.0 ||
-        stats1.miss_rate == 100.0
-    )
-
-    assert(stats2.calls.put == 2)
-    assert(stats2.operations == 5)
-    assert(stats2.hit_rate == 50.0)
-  end
 end
diff --git a/test/cachex/limit/accessed_test.exs b/test/cachex/limit/accessed_test.exs
new file mode 100644
index 0000000..dad13f8
--- /dev/null
+++ b/test/cachex/limit/accessed_test.exs
@@ -0,0 +1,30 @@
+defmodule Cachex.Limit.AccessedTest do
+  use Cachex.Test.Case
+
+  # Basic coverage of the access hook to ensure that modification
+  # times are refreshed on a read call for a key. More specific
+  # tests will be added as new test cases and issues arise.
+  test "updating modification times on read access" do
+    # create a test cache using the LRW access hook to refresh modification
+    cache = TestUtils.create_cache(hooks: [hook(module: Cachex.Limit.Accessed)])
+
+    # create a new key to check against
+    {:ok, true} = Cachex.put(cache, "key", 1)
+
+    # fetch the raw modification time of the cache entry
+    entry(modified: modified1) = Cachex.inspect!(cache, {:entry, "key"})
+
+    # wait a while...
+    :timer.sleep(50)
+
+    # fetch back the key again
+    {:ok, 1} = Cachex.get(cache, "key")
+
+    # the modification time should update...
+    TestUtils.poll(250, true, fn ->
+      cache
+      |> Cachex.inspect!({:entry, "key"})
+      |> entry(:modified) != modified1
+    end)
+  end
+end
diff --git a/test/cachex/policy/lrw/evented_test.exs b/test/cachex/limit/evented_test.exs
similarity index 54%
rename from test/cachex/policy/lrw/evented_test.exs
rename to test/cachex/limit/evented_test.exs
index 5c53648..4dea1c9 100644
--- a/test/cachex/policy/lrw/evented_test.exs
+++ b/test/cachex/limit/evented_test.exs
@@ -1,4 +1,4 @@
-defmodule Cachex.Policy.LRW.EventedTest do
+defmodule Cachex.Limit.EventedTest do
   use Cachex.Test.Case
 
   # This test just ensures that there are no artificial limits placed on a cache
@@ -32,17 +32,23 @@ defmodule Cachex.Policy.LRW.EventedTest do
     # create a forwarding hook
     hook = ForwardHook.create()
 
-    # define our cache limit
-    limit =
-      limit(
-        size: 100,
-        policy: Cachex.Policy.LRW,
-        reclaim: 0.75,
-        options: [batch_size: 25, immediate: true]
-      )
-
     # create a cache with a max size
-    cache = TestUtils.create_cache(hooks: [hook], limit: limit)
+    cache =
+      TestUtils.create_cache(
+        hooks: [
+          hook,
+          hook(
+            module: Cachex.Limit.Evented,
+            args: {
+              100,
+              [
+                reclaim: 0.75,
+                batch_size: 25
+              ]
+            }
+          )
+        ]
+      )
 
     # retrieve the cache state
     state = Services.Overseer.retrieve(cache)
@@ -112,80 +118,4 @@ defmodule Cachex.Policy.LRW.EventedTest do
     # just ensure that notifying errors to the policy doesn't cause a crash
     Services.Informant.notify([hook1], {:action, []}, {:error, false})
   end
-
-  # This test ensures that the cache eviction policy will evict any expired values
-  # before removing the oldest. This is to make sure that we don't remove anything
-  # without good reason. To verify this we add 50 keys with a TTL more recently
-  # than those without and cross the cache limit. We then validate that all expired
-  # keys have been purged, and no other keys have been removed as the purge takes
-  # the cache size back under the maximum size.
-  test "evicting by removing expired keys" do
-    # define our cache limit
-    limit =
-      limit(
-        size: 100,
-        policy: Cachex.Policy.LRW,
-        reclaim: 0.3,
-        options: [batch_size: -1, immediate: true]
-      )
-
-    # create a cache with a max size
-    cache = TestUtils.create_cache(limit: limit)
-
-    # retrieve the cache state
-    state = Services.Overseer.retrieve(cache)
-
-    # set 50 keys without ttl
-    for x <- 1..50 do
-      # set the key
-      {:ok, true} = Cachex.put(state, x, x)
-
-      # tick to make sure each has a new touch time
-      :timer.sleep(1)
-    end
-
-    # set a more recent 50 keys
-    for x <- 51..100 do
-      # set the key
-      {:ok, true} = Cachex.put(state, x, x, expire: 1)
-
-      # tick to make sure each has a new touch time
-      :timer.sleep(1)
-    end
-
-    # retrieve the cache size
-    size1 = Cachex.size!(cache)
-
-    # verify the cache size
-    assert(size1 == 100)
-
-    # add a new key to the cache to trigger evictions
-    {:ok, true} = Cachex.put(state, 101, 101)
-
-    # verify the cache shrinks to 51%
-    TestUtils.poll(250, 51, fn ->
-      Cachex.size!(state)
-    end)
-
-    # our validation step
-    validate = fn range, expected ->
-      # iterate all keys in the range
-      for x <- range do
-        # retrieve whether the key exists
-        exists = Cachex."exists?!"(state, x)
-
-        # verify whether it exists
-        assert(exists == expected)
-      end
-    end
-
-    # verify the first 50 keys are retained
-    validate.(1..50, true)
-
-    # verify the second 50 are removed
-    validate.(51..100, false)
-
-    # verify the last key added is retained
-    validate.(101..101, true)
-  end
 end
diff --git a/test/cachex/policy/lrw/scheduled_test.exs b/test/cachex/limit/scheduled_test.exs
similarity index 52%
rename from test/cachex/policy/lrw/scheduled_test.exs
rename to test/cachex/limit/scheduled_test.exs
index 5db0c92..93a2c25 100644
--- a/test/cachex/policy/lrw/scheduled_test.exs
+++ b/test/cachex/limit/scheduled_test.exs
@@ -1,4 +1,4 @@
-defmodule Cachex.Policy.LRW.ScheduledTest do
+defmodule Cachex.Limit.ScheduledTest do
   use Cachex.Test.Case
 
   # This test just ensures that there are no artificial limits placed on a cache
@@ -32,17 +32,26 @@ defmodule Cachex.Policy.LRW.ScheduledTest do
     # create a forwarding hook
     hook = ForwardHook.create()
 
-    # define our cache limit
-    limit =
-      limit(
-        size: 100,
-        policy: Cachex.Policy.LRW,
-        reclaim: 0.75,
-        options: [batch_size: 25, frequency: 100]
-      )
-
     # create a cache with a max size
-    cache = TestUtils.create_cache(hooks: [hook], limit: limit)
+    cache =
+      TestUtils.create_cache(
+        hooks: [
+          hook,
+          hook(
+            module: Cachex.Limit.Scheduled,
+            args: {
+              100,
+              [
+                reclaim: 0.75,
+                batch_size: 25
+              ],
+              [
+                frequency: 100
+              ]
+            }
+          )
+        ]
+      )
 
     # retrieve the cache state
     state = Services.Overseer.retrieve(cache)
@@ -100,80 +109,4 @@ defmodule Cachex.Policy.LRW.ScheduledTest do
     # just ensure that notifying errors to the policy doesn't cause a crash
     Services.Informant.notify([hook1], {:action, []}, {:error, false})
   end
-
-  # This test ensures that the cache eviction policy will evict any expired values
-  # before removing the oldest. This is to make sure that we don't remove anything
-  # without good reason. To verify this we add 50 keys with a TTL more recently
-  # than those without and cross the cache limit. We then validate that all expired
-  # keys have been purged, and no other keys have been removed as the purge takes
-  # the cache size back under the maximum size.
-  test "evicting by removing expired keys" do
-    # define our cache limit
-    limit =
-      limit(
-        size: 100,
-        policy: Cachex.Policy.LRW,
-        reclaim: 0.3,
-        options: [batch_size: -1, frequency: 100]
-      )
-
-    # create a cache with a max size
-    cache = TestUtils.create_cache(limit: limit)
-
-    # retrieve the cache state
-    state = Services.Overseer.retrieve(cache)
-
-    # set 50 keys without ttl
-    for x <- 1..50 do
-      # set the key
-      {:ok, true} = Cachex.put(state, x, x)
-
-      # tick to make sure each has a new touch time
-      :timer.sleep(1)
-    end
-
-    # set a more recent 50 keys
-    for x <- 51..100 do
-      # set the key
-      {:ok, true} = Cachex.put(state, x, x, expire: 1)
-
-      # tick to make sure each has a new touch time
-      :timer.sleep(1)
-    end
-
-    # retrieve the cache size
-    size1 = Cachex.size!(cache)
-
-    # verify the cache size
-    assert(size1 == 100)
-
-    # add a new key to the cache to trigger evictions
-    {:ok, true} = Cachex.put(state, 101, 101)
-
-    # verify the cache shrinks to 51%
-    TestUtils.poll(250, 51, fn ->
-      Cachex.size!(state)
-    end)
-
-    # our validation step
-    validate = fn range, expected ->
-      # iterate all keys in the range
-      for x <- range do
-        # retrieve whether the key exists
-        exists = Cachex."exists?!"(state, x)
-
-        # verify whether it exists
-        assert(exists == expected)
-      end
-    end
-
-    # verify the first 50 keys are retained
-    validate.(1..50, true)
-
-    # verify the second 50 are removed
-    validate.(51..100, false)
-
-    # verify the last key added is retained
-    validate.(101..101, true)
-  end
 end
diff --git a/test/cachex/options_test.exs b/test/cachex/options_test.exs
index 6490314..3140e3b 100644
--- a/test/cachex/options_test.exs
+++ b/test/cachex/options_test.exs
@@ -236,47 +236,6 @@ defmodule Cachex.OptionsTest do
     assert(msg == :invalid_hook)
   end
 
-  # This test ensures that the max size options can be correctly parsed. Parsing
-  # this flag will set the Limit field inside the returned state, so it needs to
-  # be checked. It will also add any Limit hooks to the hooks list, so this needs
-  # to also be verified within this test.
-  test "parsing :limit flags" do
-    # grab a cache name
-    name = TestUtils.create_name()
-
-    # create a default limit
-    default = limit()
-
-    # our cache limit
-    max_size = 500
-    c_limits = limit(size: max_size)
-
-    # parse options with a valid max_size
-    {:ok, cache(hooks: hooks1, limit: limit1)} =
-      Cachex.Options.parse(name, limit: max_size)
-
-    {:ok, cache(hooks: hooks2, limit: limit2)} =
-      Cachex.Options.parse(name, limit: c_limits)
-
-    {:ok, cache(hooks: hooks3, limit: limit3)} = Cachex.Options.parse(name, [])
-
-    # parse options with invalid max_size
-    {:error, msg} = Cachex.Options.parse(name, limit: "max_size")
-
-    # check the first and second states have limits
-    assert(limit1 == c_limits)
-    assert(limit2 == c_limits)
-    assert(hooks1 == hooks(pre: [], post: Cachex.Policy.LRW.hooks(c_limits)))
-    assert(hooks2 == hooks(pre: [], post: Cachex.Policy.LRW.hooks(c_limits)))
-
-    # check the third has no limits attached
-    assert(limit3 == default)
-    assert(hooks3 == hooks(pre: [], post: []))
-
-    # check the fourth causes an error
-    assert(msg == :invalid_limit)
-  end
-
   # This test will verify the parsing of compression flags to determine whether
   # a cache has them enabled or disabled. This is simply checking whether the flag
   # is set to true or false, and the default.
@@ -324,27 +283,6 @@ defmodule Cachex.OptionsTest do
     assert(msg == :invalid_router)
   end
 
-  # This test will verify the ability to record stats in a state. This option
-  # will just add the Cachex Stats hook to the list of hooks inside the cache.
-  # We just need to verify that the hook is added after being parsed.
-  test "parsing :stats flags" do
-    # grab a cache name
-    name = TestUtils.create_name()
-
-    # create a stats hook
-    hook =
-      hook(
-        module: Cachex.Stats,
-        name: name(name, :stats)
-      )
-
-    # parse the stats recording flags
-    {:ok, cache(hooks: hooks)} = Cachex.Options.parse(name, stats: true)
-
-    # ensure the stats hook has been added
-    assert(hooks == hooks(pre: [], post: [hook]))
-  end
-
   # This test will verify the parsing of transactions flags to determine whether
   # a cache has them enabled or disabled. This is simply checking whether the flag
   # is set to true or false, and the default. We also verify that the transaction
diff --git a/test/cachex/policy/lru/evented_test.exs b/test/cachex/policy/lru/evented_test.exs
deleted file mode 100644
index 904817f..0000000
--- a/test/cachex/policy/lru/evented_test.exs
+++ /dev/null
@@ -1,110 +0,0 @@
-defmodule Cachex.Policy.LRU.EventedTest do
-  use Cachex.Test.Case
-
-  test "evicting when a cache crosses a limit" do
-    # create a forwarding hook
-    hook = ForwardHook.create()
-
-    # define our cache limit
-    limit =
-      limit(
-        size: 100,
-        policy: Cachex.Policy.LRU,
-        reclaim: 0.75,
-        options: [batch_size: 25, immediate: true, silent: false]
-      )
-
-    # create a cache with a max size
-    cache = TestUtils.create_cache(hooks: [hook], limit: limit)
-
-    # retrieve the cache state
-    state = Services.Overseer.retrieve(cache)
-
-    # add 100 keys to the cache
-    for x <- 1..100 do
-      # add the entry to the cache
-      {:ok, true} = Cachex.put(state, x, x)
-
-      # tick to make sure each has a new touch time
-      :timer.sleep(1)
-    end
-
-    # retrieve the cache size
-    size1 = Cachex.size!(cache)
-
-    # verify the cache size
-    assert(size1 == 100)
-
-    # flush all existing hook events
-    TestUtils.flush()
-
-    # run a no-op fetch to verify no change
-    {:ignore, nil} =
-      Cachex.fetch(state, 101, fn ->
-        {:ignore, nil}
-      end)
-
-    # retrieve the cache size
-    size2 = Cachex.size!(cache)
-
-    # verify the cache size
-    assert(size2 == 100)
-
-    # store the current mod time
-    previous = :os.system_time(1000)
-
-    # tick to change ms
-    :timer.sleep(1)
-
-    # read the first key to re-touch
-    {:ok, 1} = Cachex.get(state, 1)
-
-    # wait for the touch async
-    TestUtils.poll(250, true, fn ->
-      modified =
-        state
-        |> Cachex.inspect!({:entry, 1})
-        |> entry(:modified)
-
-      assert modified > previous
-    end)
-
-    # add a new key to the cache to trigger evictions
-    {:ok, true} = Cachex.put(state, 101, 101)
-
-    # verify the cache shrinks to 25%
-    TestUtils.poll(250, 25, fn ->
-      Cachex.size!(state)
-    end)
-
-    # our validation step
-    validate = fn range, expected ->
-      # iterate all keys in the range
-      for x <- range do
-        # retrieve whether the key exists
-        exists = Cachex."exists?!"(state, x)
-
-        # verify whether it exists
-        assert(exists == expected)
-      end
-    end
-
-    # verify the 1st key was refreshed
-    validate.(1..1, true)
-
-    # verify the next 76 keys are removed
-    validate.(2..77, false)
-
-    # verify the last 24 are retained
-    validate.(78..101, true)
-
-    # finally, verify hooks are notified
-    assert_receive({{:clear, [[]]}, {:ok, 76}})
-
-    # retrieve the policy hook definition
-    cache(hooks: hooks(post: [hook1 | _])) = state
-
-    # just ensure that notifying errors to the policy doesn't cause a crash
-    Services.Informant.notify([hook1], {:action, []}, {:error, false})
-  end
-end
diff --git a/test/cachex/policy/lru/scheduled_test.exs b/test/cachex/policy/lru/scheduled_test.exs
deleted file mode 100644
index f8ce539..0000000
--- a/test/cachex/policy/lru/scheduled_test.exs
+++ /dev/null
@@ -1,92 +0,0 @@
-defmodule Cachex.Policy.LRU.ScheduledTest do
-  use Cachex.Test.Case
-
-  test "evicting when a cache crosses a limit" do
-    # create a forwarding hook
-    hook = ForwardHook.create()
-
-    # define our cache limit
-    limit =
-      limit(
-        size: 100,
-        policy: Cachex.Policy.LRU,
-        reclaim: 0.75,
-        options: [batch_size: 25, frequency: 100]
-      )
-
-    # create a cache with a max size
-    cache = TestUtils.create_cache(hooks: [hook], limit: limit)
-
-    # retrieve the cache state
-    state = Services.Overseer.retrieve(cache)
-
-    # add 1000 keys to the cache
-    for x <- 1..100 do
-      # add the entry to the cache
-      {:ok, true} = Cachex.put(state, x, x)
-
-      # tick to make sure each has a new touch time
-      :timer.sleep(1)
-    end
-
-    # retrieve the cache size
-    size1 = Cachex.size!(cache)
-
-    # verify the cache size
-    assert(size1 == 100)
-
-    # flush all existing hook events
-    TestUtils.flush()
-
-    # store the current mod time
-    previous = :os.system_time(1000)
-
-    # tick to change ms
-    :timer.sleep(1)
-
-    # read the first key to re-touch
-    {:ok, 1} = Cachex.get(state, 1)
-
-    # wait for the touch async
-    TestUtils.poll(250, true, fn ->
-      modified =
-        state
-        |> Cachex.inspect!({:entry, 1})
-        |> entry(:modified)
-
-      assert modified > previous
-    end)
-
-    # add a new key to the cache to trigger evictions
-    {:ok, true} = Cachex.put(state, 101, 101)
-
-    # verify the cache shrinks to 25%
-    TestUtils.poll(250, 25, fn ->
-      Cachex.size!(state)
-    end)
-
-    # our validation step
-    validate = fn range, expected ->
-      # iterate all keys in the range
-      for x <- range do
-        # retrieve whether the key exists
-        exists = Cachex."exists?!"(state, x)
-
-        # verify whether it exists
-        assert(exists == expected)
-      end
-    end
-
-    # verify the 1st key was refreshed
-    validate.(1..1, true)
-
-    # verify the next 76 keys are removed
-    validate.(2..77, false)
-
-    # verify the last 24 are retained
-    validate.(78..101, true)
-
-    # finally, verify hooks are notified
-    assert_receive({{:clear, [[]]}, {:ok, 76}})
-  end
-end
diff --git a/test/cachex/policy_test.exs b/test/cachex/policy_test.exs
deleted file mode 100644
index fdc23b8..0000000
--- a/test/cachex/policy_test.exs
+++ /dev/null
@@ -1,10 +0,0 @@
-defmodule Cachex.PolicyTest do
-  use Cachex.Test.Case
-
-  test "default policy implementations" do
-    assert __MODULE__.DefaultPolicy.hooks(limit()) == []
-  end
-
-  defmodule DefaultPolicy,
-    do: use(Cachex.Policy)
-end
diff --git a/test/cachex/services/informant_test.exs b/test/cachex/services/informant_test.exs
index db05ce0..869f402 100644
--- a/test/cachex/services/informant_test.exs
+++ b/test/cachex/services/informant_test.exs
@@ -78,32 +78,43 @@ defmodule Cachex.Services.InformantTest do
     # create a synchronous hook
     hook4 = ExecuteHook.create(:informant_execute_hook_sync_timeout)
 
+    # create a named hook
+    hook5 =
+      ForwardHook.create(
+        :informant_forward_hook_pre,
+        :informant_forward_hook_pre_named
+      )
+
     # create a hook without initializing
-    hook5 = ForwardHook.create()
+    hook6 = ForwardHook.create()
 
     # initialize caches to initialize the hooks
     cache1 = TestUtils.create_cache(hooks: hook1)
     cache2 = TestUtils.create_cache(hooks: hook2)
     cache3 = TestUtils.create_cache(hooks: hook3)
     cache4 = TestUtils.create_cache(hooks: hook4)
+    cache5 = TestUtils.create_cache(hooks: hook5)
 
     # update our hooks from the caches
     cache(hooks: hooks(pre: [hook1])) = Services.Overseer.retrieve(cache1)
     cache(hooks: hooks(post: [hook2])) = Services.Overseer.retrieve(cache2)
     cache(hooks: hooks(post: [hook3])) = Services.Overseer.retrieve(cache3)
     cache(hooks: hooks(post: [hook4])) = Services.Overseer.retrieve(cache4)
+    cache(hooks: hooks(pre: [hook5])) = Services.Overseer.retrieve(cache5)
 
     # uninitialized hooks shouldn't emit
-    Services.Informant.notify([hook5], {:action, []}, :result)
+    Services.Informant.notify([hook6], {:action, []}, :result)
 
     # ensure nothing is received
     refute_receive({{:action, []}, :result})
 
     # pre hooks only ever get the action
     Services.Informant.notify([hook1], {:pre_hooks, []}, :result)
+    Services.Informant.notify([hook5], {:pre_hooks, []}, :result)
 
     # ensure only the action is received
     assert_receive({{:pre_hooks, []}, :result})
+    assert_receive({{:pre_hooks, []}, :result})
 
     # post hooks can receive results if requested
     Services.Informant.notify([hook2], {:post_hooks, []}, :result)
diff --git a/test/cachex/spec/validator_test.exs b/test/cachex/spec/validator_test.exs
index 47249a4..5183cde 100644
--- a/test/cachex/spec/validator_test.exs
+++ b/test/cachex/spec/validator_test.exs
@@ -158,32 +158,6 @@ defmodule Cachex.Spec.ValidatorTest do
     refute Validator.valid?(:hooks, hooks10)
   end
 
-  test "validation of limit records" do
-    # define some valid records
-    limit1 = limit(size: 100)
-    limit2 = limit(size: nil)
-
-    # ensure all records are valid
-    assert Validator.valid?(:limit, limit1)
-    assert Validator.valid?(:limit, limit2)
-
-    # define some invalid records
-    limit3 = limit(size: -1)
-    limit4 = limit(policy: :missing)
-    limit5 = limit(reclaim: 0.0)
-    limit6 = limit(reclaim: 1.1)
-    limit7 = limit(options: nil)
-    limit8 = limit(options: [1])
-
-    # ensure all records are invalid
-    refute Validator.valid?(:limit, limit3)
-    refute Validator.valid?(:limit, limit4)
-    refute Validator.valid?(:limit, limit5)
-    refute Validator.valid?(:limit, limit6)
-    refute Validator.valid?(:limit, limit7)
-    refute Validator.valid?(:limit, limit8)
-  end
-
   test "validation of router records" do
     # define some valid records
     router1 = router()
diff --git a/test/cachex/spec_test.exs b/test/cachex/spec_test.exs
index 174d7eb..21a116c 100644
--- a/test/cachex/spec_test.exs
+++ b/test/cachex/spec_test.exs
@@ -16,9 +16,6 @@ defmodule Cachex.SpecTest do
   test "default hooks record values",
     do: assert(hooks() == {:hooks, [], []})
 
-  test "default limit record values",
-    do: assert(limit() == {:limit, nil, Cachex.Policy.LRW, 0.1, []})
-
   test "generating constants via macros" do
     assert const(:local) == [local: true]
     assert const(:notify_false) == [notify: false]
@@ -27,7 +24,7 @@ defmodule Cachex.SpecTest do
 
     assert const(:purge_override) == [
              via: const(:purge_override_call),
-             hook_result: const(:purge_override_result)
+             result: const(:purge_override_result)
            ]
 
     assert const(:table_options) == [
diff --git a/test/cachex/stats_test.exs b/test/cachex/stats_test.exs
index df03f5d..1e6b8d8 100644
--- a/test/cachex/stats_test.exs
+++ b/test/cachex/stats_test.exs
@@ -8,7 +8,12 @@ defmodule Cachex.StatsTest do
   # by 1 (as clearing is a single cache op).
   test "registering clear actions" do
     # create a test cache
-    cache = TestUtils.create_cache(stats: true)
+    cache =
+      TestUtils.create_cache(
+        hooks: [
+          hook(module: Cachex.Stats)
+        ]
+      )
 
     # set a few values in the cache
     for i <- 0..4 do
@@ -40,7 +45,12 @@ defmodule Cachex.StatsTest do
   # the result of the call (which is either true or false) under the del namespace.
   test "registering delete actions" do
     # create a test cache
-    cache = TestUtils.create_cache(stats: true)
+    cache =
+      TestUtils.create_cache(
+        hooks: [
+          hook(module: Cachex.Stats)
+        ]
+      )
 
     # set a few values in the cache
     for i <- 0..1 do
@@ -73,7 +83,12 @@ defmodule Cachex.StatsTest do
   # the global namespace based on whether the key exists or not.
   test "registering exists? actions" do
     # create a test cache
-    cache = TestUtils.create_cache(stats: true)
+    cache =
+      TestUtils.create_cache(
+        hooks: [
+          hook(module: Cachex.Stats)
+        ]
+      )
 
     # set a value in the cache
     {:ok, true} = Cachex.put(cache, 1, 1)
@@ -106,7 +121,12 @@ defmodule Cachex.StatsTest do
   # based on whether the key was in the cache.
   test "registering get actions" do
     # create a test cache
-    cache = TestUtils.create_cache(stats: true)
+    cache =
+      TestUtils.create_cache(
+        hooks: [
+          hook(module: Cachex.Stats)
+        ]
+      )
 
     # set a value in the cache
     {:ok, true} = Cachex.put(cache, 1, 1)
@@ -140,7 +160,12 @@ defmodule Cachex.StatsTest do
   # will also increment the miss count (as a key must miss in order to fall back).
   test "registering fetch actions" do
     # create a test cache
-    cache = TestUtils.create_cache(stats: true)
+    cache =
+      TestUtils.create_cache(
+        hooks: [
+          hook(module: Cachex.Stats)
+        ]
+      )
 
     # set a value in the cache
     {:ok, true} = Cachex.put(cache, 1, 1)
@@ -177,7 +202,12 @@ defmodule Cachex.StatsTest do
   # increment the updateCount. Both increment the operation count.
   test "registering incr/decr actions" do
     # create a test cache
-    cache = TestUtils.create_cache(stats: true)
+    cache =
+      TestUtils.create_cache(
+        hooks: [
+          hook(module: Cachex.Stats)
+        ]
+      )
 
     # incr values in the cache
     {:ok, 5} = Cachex.incr(cache, 1, 3, default: 2)
@@ -219,10 +249,12 @@ defmodule Cachex.StatsTest do
     # create a test cache
     cache =
       TestUtils.create_cache(
-        stats: true,
         commands: [
           last: command(type: :read, execute: last),
           lpop: command(type: :write, execute: lpop)
+        ],
+        hooks: [
+          hook(module: Cachex.Stats)
         ]
       )
 
@@ -265,7 +297,12 @@ defmodule Cachex.StatsTest do
   # evictionCount. This is because purged keys are removed due to TTL expiration.
   test "registering purge actions" do
     # create a test cache
-    cache = TestUtils.create_cache(stats: true)
+    cache =
+      TestUtils.create_cache(
+        hooks: [
+          hook(module: Cachex.Stats)
+        ]
+      )
 
     # set a few values in the cache
     for i <- 0..4 do
@@ -301,7 +338,12 @@ defmodule Cachex.StatsTest do
   # set namespace, in order to avoid false positives.
   test "registering put actions" do
     # create a test cache
-    cache = TestUtils.create_cache(stats: true)
+    cache =
+      TestUtils.create_cache(
+        hooks: [
+          hook(module: Cachex.Stats)
+        ]
+      )
 
     # set a few values in the cache
     for i <- 0..4 do
@@ -327,7 +369,12 @@ defmodule Cachex.StatsTest do
   # writing a batch will correctly count using the length of the batch itself.
   test "registering put_many actions" do
     # create a test cache
-    cache = TestUtils.create_cache(stats: true)
+    cache =
+      TestUtils.create_cache(
+        hooks: [
+          hook(module: Cachex.Stats)
+        ]
+      )
 
     # set a few values in the cache
     {:ok, true} =
@@ -360,7 +407,12 @@ defmodule Cachex.StatsTest do
   # missCount instead. Both also increment keys inside the take namespace.
   test "registering take actions" do
     # create a test cache
-    cache = TestUtils.create_cache(stats: true)
+    cache =
+      TestUtils.create_cache(
+        hooks: [
+          hook(module: Cachex.Stats)
+        ]
+      )
 
     # set a value in the cache
     {:ok, true} = Cachex.put(cache, 1, 1)
@@ -393,7 +445,12 @@ defmodule Cachex.StatsTest do
   # This test verifies the update actions and the incremenation of the necessary keys.
   test "registering update actions" do
     # create a test cache
-    cache = TestUtils.create_cache(stats: true)
+    cache =
+      TestUtils.create_cache(
+        hooks: [
+          hook(module: Cachex.Stats)
+        ]
+      )
 
     # set a value in the cache
     {:ok, true} = Cachex.put(cache, 1, 1)
@@ -416,33 +473,6 @@ defmodule Cachex.StatsTest do
     )
   end
 
-  # There's nothing more to test inside this hook beyond the ability to retrieve
-  # the current state of the hook, and validate what it looks like after a couple
-  # of stats have been incremented. Incrementation is done via the Cachex.Stats
-  # module, so please refer to those tests for any issues with counters.
-  test "retrieving the state of a stats hook" do
-    # create a test cache
-    cache = TestUtils.create_cache(stats: true)
-    cache = Services.Overseer.retrieve(cache)
-
-    # retrieve the current time
-    ctime = now()
-
-    # carry out some cache operations
-    {:ok, true} = Cachex.put(cache, 1, 1)
-    {:ok, 1} = Cachex.get(cache, 1)
-
-    # attempt to retrieve the cache stats
-    {:ok, stats} = Cachex.Stats.retrieve(cache)
-
-    # verify the state of the stats
-    assert_in_delta(stats.meta.creation_date, ctime, 5)
-    assert(stats.calls == %{get: 1, put: 1})
-    assert(stats.hits == 1)
-    assert(stats.writes == 1)
-    assert(stats.operations == 2)
-  end
-
   # Retrieves stats with no :meta field
   defp stats_no_meta(cache) do
     with {:ok, stats} <- Cachex.stats(cache) do
diff --git a/test/cachex/test/hook/execute.ex b/test/cachex/test/hook/execute.ex
index 8e3d21a..4f084de 100644
--- a/test/cachex/test/hook/execute.ex
+++ b/test/cachex/test/hook/execute.ex
@@ -17,8 +17,8 @@ defmodule Cachex.Test.Hook.Execute do
   @doc """
   Returns a hook definition for a custom execute hook.
   """
-  def create(module) when is_atom(module),
-    do: hook(module: module, state: self())
+  def create(module, name \\ nil) when is_atom(module),
+    do: hook(module: module, args: self(), name: name)
 
   @doc """
   Binds a module for a given name and provided overrides.
@@ -40,7 +40,6 @@ defmodule Cachex.Test.Hook.Execute do
         defmodule unquote(name) do
           use Cachex.Hook
 
-          # apply configuration overrides
           def async?,
             do: unquote(async)
 
diff --git a/test/cachex/test/hook/forward.ex b/test/cachex/test/hook/forward.ex
index dc7d673..62cba32 100644
--- a/test/cachex/test/hook/forward.ex
+++ b/test/cachex/test/hook/forward.ex
@@ -17,8 +17,8 @@ defmodule Cachex.Test.Hook.Forward do
   @doc """
   Returns a hook definition for a custom forward hook.
   """
-  def create(module) when is_atom(module),
-    do: hook(module: module, state: self())
+  def create(module, name \\ nil) when is_atom(module),
+    do: hook(module: module, args: self(), name: name)
 
   @doc """
   Binds a module for a given name and provided overrides.
@@ -40,7 +40,6 @@ defmodule Cachex.Test.Hook.Forward do
         defmodule unquote(name) do
           use Cachex.Hook
 
-          # apply configuration overrides
           def async?,
             do: unquote(async)
 
diff --git a/test/cachex_test.exs b/test/cachex_test.exs
index 09dbe17..d7ab0b6 100644
--- a/test/cachex_test.exs
+++ b/test/cachex_test.exs
@@ -166,7 +166,7 @@ defmodule CachexTest do
     assert(is_even(length(definitions)))
 
     # verify the size to cause errors on addition/removal
-    assert(length(definitions) == 146)
+    assert(length(definitions) == 150)
 
     # validate all definitions
     for {name, arity} <- definitions do