Use when designing a module interface, when it exports nearly as much as it hides or is as complicated as its implementation, or when tiny one-function modules multiply.
Installs into .claude/skills of the current project.
Are you the author of Functional Designing Deep Modules?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/yutna-functional-designing-deep-modules)
---
name: functional-designing-deep-modules
description: Use when designing a module interface, when it exports nearly as much as it hides or is as complicated as its implementation, or when tiny one-function modules multiply.
license: MIT
metadata:
pack: functional-design-skills
version: 3.3.0
---
# Designing Deep Modules
**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
A module is two things: an interface, which is everything a caller must
know to use it, and an implementation, which is everything else. A
module's value is the ratio between them. Deep modules present a small
interface over a lot of functionality; shallow modules present an
interface almost as complicated as the code behind it, and so pay their
own cost back in complexity.
The best modules are deep. Getting depth wrong is the most common
structural mistake in otherwise clean functional code, because splitting
feels virtuous and each split adds an interface.
## When to use
- Deciding what a new module or file exports
- Reviewing an interface before other code depends on it
- A codebase has many small files that each do almost nothing
- A wrapper exists that only forwards to something else
- Deciding whether a helper deserves its own module
Not for: deciding whether two existing pieces of code belong together,
which is
[functional-splitting-and-joining-code](../functional-splitting-and-joining-code/SKILL.md).
## What counts as the interface
In functional code the interface is larger than the export list. It is
everything a caller must know, formal and informal:
| Part of the interface | Formal? | Example |
| --------------------- | ------- | ----------------------------- |
| Exported signatures | Yes | `price : Booking -> Priced` |
| Types in signatures | Yes | the fields of `Booking` |
| Failure cases | Yes | the `E` in `Result<T, E>` |
| Effects | Yes | `Async`, `IO` in the type |
| Required call order | No | "validate before price" |
| Value constraints | No | "quantity must be positive" |
| Performance promises | No | "safe to call in a loop" |
| Idempotency | No | "calling twice double-counts" |
Every informal item is a cost a caller pays and a compiler cannot help
with. The design goal is to make the formal parts carry as much of the
contract as possible, and to reduce the rest to nothing.
## Core rules
1. Default. **Measure depth as functionality hidden per unit of interface.**
Count the interface, including the informal parts above. If the count
approaches the size of the implementation, the module is not earning its
existence. What to do about it depends on who calls it: a shallow module that
several callers share is not merged into any of them, and
[depth-audit.md](references/depth-audit.md) says what happens to it instead.
2. Default. **Prefer fewer, larger modules over many tiny ones.** Each boundary
costs an interface. A boundary must buy more than it costs. It has bought it
in the cases
[functional-splitting-and-joining-code](../functional-splitting-and-joining-code/SKILL.md)
lists under "Keep apart when", and where a small module owns a type together
with its constructor.
3. Default. **Design the interface for the common case.** Make the frequent use
a one-liner; let the rare use be more work, not the other way round. Where
the callers that exist want different outcomes, there is no common case:
expose two named functions, not one with a flag.
4. Rule. **Do not export what callers should not use.** Test through the
interface. Rule 1 of
[functional-deciding-what-matters](../functional-deciding-what-matters/SKILL.md)
covers the module that already exports every helper it has. See
[functional-hiding-information](../functional-hiding-information/SKILL.md).
5. Judgement. **Make it somewhat general purpose.** Not maximally general, and
not welded to today's single caller. See
[general-purpose-questions.md](references/general-purpose-questions.md).
6. Default. **Reject pure pass-throughs.** A function whose body is one call
with the same arguments adds interface and hides nothing. Rule 1 of
[functional-separating-layers](../functional-separating-layers/SKILL.md)
names the one-call functions that stay.
## Pattern
A shallow module: the interface restates the implementation.
```text
-- BookingStore: five exported functions, each one line of real work
insertBookingRow : BookingRow -> AsyncResult<Unit, DbError>
selectBookingRow : BookingId -> AsyncResult<Option<BookingRow>, DbError>
updateBookingRow : BookingRow -> AsyncResult<Unit, DbError>
beginTx : Unit -> AsyncResult<Tx, DbError>
commitTx : Tx -> AsyncResult<Unit, DbError>
```
Every caller must know about rows, transactions, and the order in which
to use them. The module hides the SQL string and nothing else. Callers
carry the complexity.
A deep module: the same functionality, one thing to know.
```text
-- BookingStore
saveBooking : Booking -> AsyncResult<Unit, SaveError>
loadBooking : BookingId -> AsyncResult<Option<Booking>, StoreError>
```
Transactions, rows, retries, and the mapping between `Booking` and storage
are now implementation. The interface is two functions and two error
types, and a change to the schema cannot reach a caller.
## Classitis and its functional twin
The belief that more, smaller units are automatically better. In
object-oriented code it produces classes with one method; in functional
code it produces:
- One-function files whose export is a rename of an import
- "Utils" modules that exist so a two-line helper has a home
- Pipelines split into steps so small that the wiring is longer than the
work
- A module for one record field's validation that holds a single check
and no type of its own. A module that owns a type with its constructor,
such as one for `TreatmentCode`, is not this
The test: could this module's contents be inlined into its single caller
without any reader losing information? If yes, it is not a module; it is
a paragraph that was given a filename.
Length is not the enemy. A long function that does one thing at one level
of abstraction is fine. Splitting it is only an improvement if each part
is independently meaningful and the split boundaries are natural.
## Red flags
- The export list is as long as the file
- Exported functions map one-to-one onto internal steps
- A caller always calls two of the exports in sequence
- Adding a feature means adding an export
- The module's doc comment lists its functions instead of its purpose
- Anything named `helpers`, `utils`, `common`, or `misc`
## Common mistakes
- **Splitting by structure rather than by knowledge.** One module per
entity or per file type, or a folder per layer for every entity,
distributes one decision across many places.
- **Counting only exported names as the interface.** Required call order
and value constraints cost callers just as much.
- **Making a module maximally general.** Generality nobody uses is
interface nobody needs.
- **Confusing a deep module with a big one.** Depth is a ratio, not a
size. A deep module can be small if what it hides is genuinely hard.
## Related skills
- [functional-hiding-information](../functional-hiding-information/SKILL.md)
- [functional-separating-layers](../functional-separating-layers/SKILL.md)
- [functional-splitting-and-joining-code](../functional-splitting-and-joining-code/SKILL.md)
- [functional-deciding-what-matters](../functional-deciding-what-matters/SKILL.md)
## Further reading
- [depth-audit.md](references/depth-audit.md) scores a module's depth and
says what to do with each score.
- [general-purpose-questions.md](references/general-purpose-questions.md)
is the set of questions that finds the right level of generality.