Use when adjacent layers repeat the same abstraction, when a function only forwards, when callers repeat the same lines after it, or when a utility carries a feature's policy.
Installs into .claude/skills of the current project.
Are you the author of Functional Separating Layers?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/yutna-functional-separating-layers)
---
name: functional-separating-layers
description: Use when adjacent layers repeat the same abstraction, when a function only forwards, when callers repeat the same lines after it, or when a utility carries a feature's policy.
license: MIT
metadata:
pack: functional-design-skills
version: 3.3.0
---
# Separating Layers
**In this pack:** [how much design a task warrants](../functional-design/references/calibration.md),
[the notation](../functional-design/references/notation.md), and
[syntax per language](../functional-design/SKILL.md#language-packs).
## Overview
In a well-structured system each layer speaks a different language. The
layer above talks about bookings and patients; the layer below talks about
rows and bytes; a layer between talks about neither at once. When two
adjacent layers have the same abstraction, the boundary between them is
not earning anything, and the code is longer for no gain.
The companion rule is direction: when complexity has to live somewhere,
it belongs in the lower layer, absorbed once, rather than in every caller
above.
## When to use
- A function's body is a single call to another function
- A wrapper module exists whose exports mirror what it wraps
- Two layers use the same types and the same vocabulary
- A module returns something every caller must post-process identically
- Deciding where to put a messy detail
Not for: deciding whether two things belong in the same module, which is
[functional-splitting-and-joining-code](../functional-splitting-and-joining-code/SKILL.md).
## Core rules
1. Default. **Each layer changes the abstraction.** If the layer above and the
layer below use the same nouns, delete one of them. A layer that keeps the
nouns stays when it does one of the other things
[layer-smells.md](references/layer-smells.md) lists: it narrows an interface,
changes the failure model, adds a guarantee, or isolates an external contract
such as a wire format, a storage schema or a vendor type, even when the types
look identical today. See
[functional-crossing-io-boundaries](../functional-crossing-io-boundaries/SKILL.md).
2. Default. **Delete pass-through functions.** A function that takes the same
arguments and calls one function with them adds interface and hides nothing.
Let the caller call directly. A one-call function that does one of the things
rule 1 lists is not a pass-through, and stays.
3. Default. **Pull complexity downward.** Given a choice between a slightly
harder implementation and a slightly harder interface, make the
implementation harder. One implementer pays once; every caller pays forever.
Leave it with the callers when the ones that exist disagree on the handling,
or when it is a choice only a caller can make, such as a currency, a time
zone or a rounding rule.
4. Default. **Absorb the common case.** If every caller does the same thing
after calling you, do it for them. The two limits of rule 3 hold here as
well, and so does a third: a step that would make this module learn the
caller's domain, as a store that formats for one screen would, stays with the
caller.
5. Default. **Do not add configuration to avoid a decision.** An option exported
so callers can choose is complexity moved upward. Decide, and expose a choice
only where two callers that exist need different values. Where they need
different outcomes, that is two functions and not an option.
6. Judgement. **Keep decorators rare.** A wrapper that adds one small behaviour
is usually better as a parameter or as part of the thing it wraps.
7. Default. **Inside the domain, order the layers by how often each changes.**
Fastest at the top: rules above concepts above utilities, and every function
above what it calls. A general utility that knows a business rule has pinned
the rule to the slowest-moving thing in the domain. Which way an import
points between the domain and vendor code is the other axis, and rule 6 of
[functional-applying-solid-functionally](../functional-applying-solid-functionally/SKILL.md)
settles it. See [layer-smells.md](references/layer-smells.md).
## Pattern
Layers with the same abstraction: three hops, no translation.
```text
-- api layer
getBooking id = service.getBooking id
-- service layer
getBooking id = repo.getBooking id
-- repo layer
getBooking id = db.query "select ..." id
```
Two of these three functions exist only to be called. Every change to a
signature ripples through all three, and no reader learns anything by
following the chain.
Layers that each translate:
```text
-- api layer: HTTP words in, HTTP words out
handleGetBooking : Request -> Async<Response>
-- translates path parameters and status codes; None becomes a 404
-- application layer: booking words only; it has an effect type, so it
-- is shell code, and the pure domain sits beneath its callers
loadBooking : BookingId -> AsyncResult<Option<Booking>, StoreError>
-- knows nothing about HTTP or SQL; a missing booking is None inside Ok
-- storage layer: row words only
selectBooking : BookingId -> AsyncResult<Option<BookingRow>, DbError>
-- knows nothing about bookings as the business means them
```
Each layer now has a reason to exist: it converts one vocabulary into
another and hides the vocabulary below it.
## Pulling complexity downward
| Situation | Upward (wrong) | Downward (right) |
| --------------------- | ---------------------- | ------------------ |
| Result needs cleaning | Every caller cleans it | Returned ready |
| Failure is retryable | Caller writes retries | Adapter retries |
| Defaults nearly fixed | Caller passes them | Module defaults |
| An edge case exists | Caller checks for it | Module handles it |
| Two shapes are needed | Caller converts | Module exports two |
The limit: do not pull complexity down when the callers genuinely differ.
Absorbing a decision that only half the callers agree with produces a
mode flag, which is complexity in both places at once.
See [pulling-complexity-down.md](references/pulling-complexity-down.md).
## Red flags
- A function body is one call with the same arguments
- Layer names differ but their types are identical, and neither isolates
an external contract
- Every caller of a function follows it with the same two lines
- A module exports options rather than making a decision
- A wrapper for one function adds one small behaviour
- Following a call takes four hops before reaching real work
## Common mistakes
- **Confusing a pass-through with a boundary.** A function that
translates types, restricts a signature, or turns an exception into a
`Result` is doing work and should stay.
- **Pulling complexity down into the wrong module.** The module that
absorbs it must be the one that owns the knowledge, or you have created
leakage while removing duplication.
- **Adding a layer for symmetry.** A layer that exists because the other
side of the system has one is a pass-through with a nicer story.
- **Removing a layer that hides a real dependency.** If the wrapper is
what stops a third-party type from reaching the domain, it earns its
keep.
## Related skills
- [functional-designing-deep-modules](../functional-designing-deep-modules/SKILL.md)
- [functional-hiding-information](../functional-hiding-information/SKILL.md)
- [functional-separating-pure-core-from-shell](../functional-separating-pure-core-from-shell/SKILL.md)
- [functional-defining-errors-out-of-existence](../functional-defining-errors-out-of-existence/SKILL.md)
## Further reading
- [layer-smells.md](references/layer-smells.md) tells a real boundary
from a pass-through, and orders the layers by how often each one
changes.
- [pulling-complexity-down.md](references/pulling-complexity-down.md)
gives the decision procedure and its limits.