Installs into .claude/skills of the current project.
Are you the author of Functional Applying Solid Functionally?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/yutna-functional-applying-solid-functionally)
---
name: functional-applying-solid-functionally
description: Use when a function takes a whole service to use one part, when adding a variant forces edits everywhere, or when structuring module dependencies.
license: MIT
metadata:
pack: functional-design-skills
version: 3.3.0
---
# Applying SOLID Functionally
**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
The SOLID principles are about the direction and shape of dependencies,
not about classes. Every one of them survives translation into functions
and data, and in functional code most of them get simpler: the
abstraction that other principles reach for with interfaces and
inheritance is, here, just a function type.
## When to use
- Deciding what a function should take as parameters
- Adding a new variant, and finding many places to edit
- Two features that change for different reasons live in one module
- A module receives a large record of functions and uses two of them
- Choosing between a choice type and a passed-in function
Not for: naming, or judging interface size in isolation.
## The five, restated
| Principle | Functional statement |
| --------- | ------------------------------------------------------- |
| SRP | One function, one reason to change, one audience |
| OCP | Extend by adding code, not by editing existing code |
| LSP | Any value of a type honours the type's whole contract |
| ISP | Depend on the narrowest function type that works |
| DIP | Concrete code depends on function types, not vice versa |
## Core rules
1. Default. **Split by who asks for the change, not by what the code does.** Two
calculations that look alike and answer to different stakeholders belong in
different modules. They are one decision if one person or one event would
change them together, and two if different people would. Who asks is read
from an owners file, the issue or the user's own words. Where none of them
says, ask once, and with no answer keep one module and record the assumption.
2. Default. **Choose the axis of change before choosing the mechanism.** New
cases and new operations pull in opposite directions. See the expression
problem below. The question is owed only where something varies, and rule 3
says how to tell.
3. Default. **Make the extension point a parameter.** A function passed in is
the simplest open-closed mechanism there is, and it needs no registry, no
class, and no framework. Where nothing varies there is no extension point:
something with one implementation, which no test needs to replace and of
which the task names no second, is called directly, and a set of cases the
business has fixed is a choice type.
4. Rule. **Keep passed-in functions total and faithful.** A substitute that
throws where the type says `Result`, or that demands more of its input than
the type states, breaks every caller written against the type.
5. Default. **Pass functions, not modules.**
`LoadPrice = TreatmentCode -> AsyncResult<Price, StoreError>` is an
interface with one member. A repository record with twenty members forces
every consumer to depend on all twenty. A small record of functions is right
when they have to vary together as a set and the callee calls them all;
[lsp-isp-dip.md](references/lsp-isp-dip.md) has the test.
6. Rule. **Point dependencies at the domain.** The function types a handler
takes are declared in the domain's words; the shell supplies
implementations. Nothing in the domain imports the database, the
framework, or the transport.
## The expression problem
This decides mechanism, and getting it wrong is the usual cause of
"adding a variant means editing ten files".
```text
-- Closed to new cases, open to new operations.
-- Adding Refunded means editing every match. Adding a new
-- function over Status costs nothing.
type Status = New | Paid | Void
```
```text
-- Open to new cases, closed to new operations.
-- Adding a new payment method costs nothing. Adding a new
-- operation means changing the record and every implementation.
alias PaymentMethod = { charge: Money -> AsyncResult<Receipt, ChargeError> }
```
Rules of thumb:
- The set of cases is fixed by the business and the operations grow:
use a **choice type**. Exhaustive matching then tells you exactly what
to update when a case really is added.
- The set of cases grows, from plugins or from a widening market, and
the operations are stable: pass a **function or a small record of
functions**.
- Both grow: pick the one that grows faster to be the open axis, and
accept edits on the other. There is no mechanism that makes both free.
Choosing a choice type is not a violation of OCP. Exhaustive matching
turns "adding a case" into a task the compiler enumerates, which is the
opposite of the OCP failure mode, where a missed site fails silently.
## Pattern
Wide dependency, violating ISP and DIP at once:
```text
handleConfirmBooking :
BookingRepository -> Instant -> UnvalidatedBooking
-> AsyncResult<List<ConfirmBookingEvent>, ConfirmBookingFailure>
-- BookingRepository has 22 functions; the handler uses two of them,
-- and its module now imports the storage module
```
Narrow dependencies, defined by the domain:
```text
-- declared beside the handler, in the domain's words
alias LoadCatalogue = Unit -> AsyncResult<Catalogue, StoreError>
alias SaveEvents =
List<ConfirmBookingEvent> -> AsyncResult<Unit, StoreError>
handleConfirmBooking :
LoadCatalogue -> SaveEvents -> Instant -> UnvalidatedBooking
-> AsyncResult<List<ConfirmBookingEvent>, ConfirmBookingFailure>
```
Each capability's type has an effect, so the function that takes them is
a handler, and the rules stay in the pure `confirmBooking` it calls. The
handler states its needs in the domain's words, the shell satisfies
them, tests supply two one-line functions, and no storage type crosses
the boundary. `ConfirmBookingFailure` is defined in
[functional-designing-workflow-pipelines](../functional-designing-workflow-pipelines/SKILL.md).
See
[functional-parameterizing-dependencies](../functional-parameterizing-dependencies/SKILL.md)
for how these are supplied without threading them through every call.
## On accessors and inheritance
Two habits carried over from object-oriented code that add nothing here:
- **Getters and setters for every field.** A record of plain data needs
no accessors. Wrapping fields in functions does not hide them; it adds
interface while leaving the design decision exposed. Hide data by not
exposing the type, or by exposing operations that mean something.
- **Inheritance for reuse.** Sharing implementation by inheritance is
the tightest coupling available. Compose functions, or pass the
varying behaviour in. Reserve subtyping for genuine substitutability,
and prefer a choice type when the set of variants is known.
## Red flags
- One module changes for two different stakeholders' requests
- Adding a variant requires edits in several unrelated files
- A parameter is a whole service, repository, or context object
- A function passed as a dependency throws instead of returning `Result`
- The domain imports the framework, the driver, or the transport
- A record type exists only to hold get and set functions
## Common mistakes
- **Treating SRP as "one function does one thing".** It is about reasons
to change. Two audiences, two modules, even if the code looks similar.
- **Reaching for plugin machinery to satisfy OCP.** If the case set is
fixed, exhaustive matching is stronger and cheaper.
- **Passing a mock-shaped object.** A dependency that must be mocked with
a framework is too wide. Narrow it until a plain function suffices.
- **Inverting a dependency that never varies.** An abstraction with one
implementation, which no test replaces and of which the task names no
second, is interface for nothing.
## Related skills
- [functional-parameterizing-dependencies](../functional-parameterizing-dependencies/SKILL.md)
- [functional-designing-deep-modules](../functional-designing-deep-modules/SKILL.md)
- [functional-modeling-with-algebraic-types](../functional-modeling-with-algebraic-types/SKILL.md)
- [functional-translating-gof-patterns](../functional-translating-gof-patterns/SKILL.md)
## Further reading
- [srp-and-ocp.md](references/srp-and-ocp.md) works through separating by
audience and the open-closed mechanisms available in functional code.
- [lsp-isp-dip.md](references/lsp-isp-dip.md) covers substitutability
contracts, narrowing dependencies, and which way the arrows point.