Skip to content
Back to skills

Functional Separating Layers

ASecurity

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.

  • 2 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added October 21, 2026
ai-agentsshellsqlapi

Works with

  • api

Security analysis

A100/100

Pro scans all 3 files and shows the line behind each finding

Scanned October 21, 2026

npx -y skills add yutna/functional-design-skills --skill functional-separating-layers --agent claude-code

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.

Security grade badge for Functional Separating Layers
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/yutna-functional-separating-layers/badge)](https://www.skillsdirectory.com/skills/yutna-functional-separating-layers)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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.

Files in this skill

  • SKILL.md7.7 KB
  • references/layer-smells.md6.2 KB
  • references/pulling-complexity-down.md3.4 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.