Skip to content
Back to skills

Functional Designing Deep Modules

ASecurity

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.

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 21, 2026
ai-agentsgosqlperformance

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-designing-deep-modules --agent claude-code

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.

Security grade badge for Functional Designing Deep Modules
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/yutna-functional-designing-deep-modules/badge)](https://www.skillsdirectory.com/skills/yutna-functional-designing-deep-modules)

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-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.

Files in this skill

  • SKILL.md7.9 KB
  • references/depth-audit.md3.2 KB
  • references/general-purpose-questions.md4 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.