Encapsulation
The interface is the only contract. The implementation is a design secret.
Parnas wrote it down in 1972 and the wording has not aged: every module is characterised by its knowledge of a design decision which it hides from all others. Its interface is chosen to reveal as little as possible about its inner workings. That is the rule, and it is the foundation that makes change cheap. A unit whose implementation is private can be rewritten without touching its callers; a unit whose implementation has leaked has fused itself to every callsite that knows about its insides.
Opinion
I've watched modern codebases pretend to follow F6 because the language provides the keywords. They have private fields, protected methods, named exports. The syntactic surface looks encapsulated; the actual practice diverges. The class exposes a getter and setter for every private field, so the encapsulation is decoration. The module exports a helper a single caller uses, so the boundary is theatrical. The function takes a flag that lets the caller bypass an invariant, so the body is no longer a secret. The keyword is there; the discipline is not.
The pressure always runs the other way: more flexibility for the caller, more configurability, more introspection. Every one of those moves is easy to add and hard to remove — once a getter exists, callers find it, and the implementation behind it is fixed forever. The fix is not more keywords. It is fewer affordances: the public surface is the smallest set of operations that lets callers do their job, and any state callers do not need to see is not reachable.
Agents read the public interface the same way reviewers do, which makes every leak more expensive, not less. If the implementation has leaked, the agent reads the leak and treats it as part of the contract. Within a session that may be benign; across sessions and across teams the leak compounds into a structural dependency no-one notices until the body has to change. Encapsulation is the only practical defence against that compound: the linter floor is necessary, the review culture is what carries it.
Copy a note and link
Grab this short comment and drop it into a PR comment or an LLM chat to prompt the right change.
Hide the inside of your code. Callers should only call methods — never read or change fields directly. If state has to change, give them a method that does it safely. Always mark every field and method as `public` or `private`; never rely on the language default. /tenet/encapsulation/F6
AI eyes only
Rule: the interface is the only contract. Public state is a defect.
Reject: pairs of getters and setters that expose private fields. Reject: public mutable members. Reject: default-visibility on classes that hold state. Reject: returning internal collections by reference.
Generate: behavioural methods that operate on internal state and enforce invariants. Constructors that validate. private (or #-prefix) by default; only promote to public when a caller demonstrates the need.
Diagnostic: if removing a public member would force a single-line rename in callers, the member is leaking implementation. Replace with a behavioural method named for the operation.
Why?
- The body is free to change while the contract holds. The cost of a refactor is bounded to the unit, not its callers.
- The unit is testable through its interface alone. Encapsulation produces the boundary TS9 Boundary Mocking wants to mock at.
- Reviewers read the public surface to validate behaviour; they don't need to read the body. Smaller surfaces mean faster reviews.
- Behavioural subtyping (A11 LSP) only works when the contract is stable. F6 is what makes LSP enforceable; without encapsulation, every subtype can be broken by a parent-class internals leak.
- Private mutable state is debuggable; public mutable state is a race. Encapsulation is the cheapest form of concurrency safety we have.
- Pairs with T1 Domain-Driven Types: the type encodes the contract; encapsulation enforces that callers cannot construct invalid states by reaching into the body.
- Agent-generated code stays composable when the public surface is small. Implementation leaks turn into agent-encoded contracts that compound across sessions.
Origins
David Parnas wrote the foundational paper in 1972. On the Criteria to be Used in Decomposing Systems into Modules presented two decompositions of a single program (the KWIC index) and showed that the information hiding decomposition produced cheaper change, while the flowchart-based decomposition produced fragile code that propagated changes across modules. The argument has not been bettered: every module is characterised by its knowledge of a design decision which it hides from all others.1On the Criteria to be Used in Decomposing Systems into Modules — CMU TR CMU-CS-71-101 (August 1971); Communications of the ACM 15(12), 1053–1058 (December 1972). The foundational paper. The KWIC index example is on pp. 6–14; the information-hiding criterion is in §7 'The Criteria'.
Barbara Liskov extended the argument to types in 1987. The Liskov Substitution Principle is, at heart, an encapsulation rule: a subtype must honour the encapsulated promises of its supertype, or substitution silently breaks. LSP is what stops encapsulation being decorative; it forces the discipline through the type system, not just at the keyword level.2Data Abstraction and Hierarchy — OOPSLA '87 keynote, addendum published in ACM SIGPLAN Notices 23(5), 17–34, May 1988. Formalised as the Liskov Substitution Principle by Liskov & Wing (1994).
Bertrand Meyer 1988 operationalised encapsulation as Design by Contract: pre- conditions, post-conditions, and class invariants are the executable form of the encapsulated interface. Eiffel made these first-class language constructs; TypeScript's readonly, Rust's ownership model, and modern type-state APIs are all descendants. Contracts make the body a secret you can actually trust.3Object-Oriented Software Construction (2nd ed., Prentice-Hall 1997). Design by Contract — preconditions, postconditions, class invariants — as the executable form of the encapsulated interface. Eiffel was the first language with first-class contract syntax.
The Gamma, Helm, Johnson, Vlissides 1994 catalogue codified the practical lever: encapsulate what varies. Every behavioural pattern (Strategy, State, Command, Observer) is an encapsulation move; every structural pattern (Adapter, Bridge, Facade, Proxy) is one too. The patterns are not arbitrary; they are twenty-three solutions to the same recurring problem of which design decisions to hide and how to hide them.4Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994). 'Encapsulate what varies' is the through-line; the catalogue is twenty-three answers to where to draw the encapsulation boundary.
Joshua Bloch 2018 ported the principle into modern Java idiom as Item 16: Minimize the accessibility of classes and members. The justification is the one Parnas gave: information hiding decouples the components that comprise a system, allowing them to be developed, tested, optimised, used, understood, and modified in isolation. Forty-six years separate the two formulations; the rule is the same.5Effective Java, 3rd ed. (Addison-Wesley, 2018). Item 16 'Minimize the accessibility of classes and members' is the modern Java textbook formulation; Item 15 'Minimize mutability' is the value-object pair.
Quotes
Every module in the second decomposition is characterized by its knowledge of a design decision which it hides from all others. Its interface or definition was chosen to reveal as little as possible about its inner workings.
The central idea of Design by Contract is a metaphor on how elements of a software system collaborate with each other on the basis of mutual obligations and benefits.
Encapsulate the concept that varies. Favor object composition over class inheritance.
Minimise the accessibility of classes and members. Information hiding decouples the components that comprise a system, allowing them to be developed, tested, optimised, used, understood, and modified in isolation.
Evidence
Twenty external sources, ranked by author authority. The first five are the canon; expand to see the rest, including the qualifiers and the named opposers. Each links out to its primary source.
- 01The foundational paper. Information hiding as the criterion for decomposition; the KWIC example shows the cost of getting it wrong.
- 02Data Abstraction and HierarchySupportsBehavioural subtyping. Encapsulation enforced through types: a subtype must honour the supertype's encapsulated contract or substitution breaks.
- 03Design by Contract — preconditions, postconditions, invariants — is the executable form of the encapsulated interface.
- 04'Encapsulate what varies' is the catalogue's through-line. Every pattern is an encapsulation move.
- 05The modern Java textbook formulation. The rationale is Parnas's, undimmed by 46 years.
Fifteen sources across five decades. The supporting line runs unbroken from Parnas in 1972 through Liskov, Meyer, GoF, and Bloch. The qualifying voice extends rather than challenges: shallow modules with trivial encapsulation are no improvement over leaks; the operational target is deep modules with substantial behaviour behind narrow interfaces. The corpus has no opposing line. Encapsulation is the most stable principle in the Foundations.
Examples
// Before: callers maintain the invariant by hand. One forgets and total drifts.class ShoppingCart { items: Item[] = []; total: number = 0;}// caller code, scattered across files:cart.items.push(item);cart.total += item.price;// elsewhere — the second caller forgets:otherCart.items.push(item); // total now lies
// After: state is private; the invariant lives inside the unit.class ShoppingCart { private readonly items: Item[] = []; private total = 0; public add(item: Item): void { this.items.push(item); this.total += item.price; } public getTotal(): number { return this.total; }}// caller code: invariant cannot drift.cart.add(item);
Enforcement
Apply these rules in eslint.config.mjs. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| @typescript-eslint/explicit-member-accessibility | typescript-eslint | class members without an explicit access modifier — public-by-default is a leak by accident. |
| @typescript-eslint/no-explicit-any | typescript-eslint | any-typed return values and parameters that erase the contract the interface promised. |
| @typescript-eslint/prefer-readonly | typescript-eslint | private fields never reassigned — should be readonly, signalling immutability after construction. |
| @typescript-eslint/no-non-null-assertion | typescript-eslint | the `!` operator — a lie to the compiler about your invariant. Prove it with a guard. |
| no-unused-private-class-members | ESLint core | private members never read — dead encapsulated state. |
eslint.config.mjsconfiguration snippet
import tseslint from 'typescript-eslint';
export default tseslint.config({
files: ['**/*.{ts,tsx}'],
rules: {
// Encapsulation hygiene — no leaking implementation as 'any'.
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/explicit-module-boundary-types': 'error',
'@typescript-eslint/no-non-null-assertion': 'error',
// Class-level encapsulation — fields must declare visibility.
'@typescript-eslint/explicit-member-accessibility': [
'error',
{ accessibility: 'explicit' },
],
'@typescript-eslint/parameter-properties': ['error', { prefer: 'parameter-property' }],
'no-unused-private-class-members': 'error',
// No mutable public state.
'@typescript-eslint/prefer-readonly': 'error',
'@typescript-eslint/prefer-readonly-parameter-types': 'off', // expensive; opt in per repo
}
});AI rules
.cursor/rules/f6-encapsulation.mdc---
description: Prickles F6 — Encapsulation / Information Hiding
globs: "**/*.{ts,tsx,js,jsx,py,java,php}"
alwaysApply: false
---
## Prickles F6 — Encapsulation / Information Hiding
Hide the inside of your code. Callers should only call methods — never read or change fields directly. The method names are the contract; the body of the code is private and free to change.
If state has to change, give callers a method that does the change safely. The method enforces the rules; the field stays private. Don't expose a field for the caller to update by hand.
Always mark every field and method as `public` or `private` on purpose. Never rely on the language default. If it's only used inside the unit, mark it `private`.
F6 is not the same as F1. F1 says each unit does one thing. F6 says each unit keeps its insides private. A focused class with public mutable fields passes F1 and fails F6 — they catch different mistakes.
Why it matters: hidden internals make change cheap. You can rewrite the body without breaking any caller, because no caller depends on the body. Leaked internals fuse the unit to every callsite that touched them — and every change becomes a graph search.Repo layout, CI, and ESLint wiring for these paths live on /implementation — not repeated on every tenet.
Counter-argument
The steelman has two heads. The first is Ousterhout's: shallow modules with ceremonial encapsulation (getter for every field, public method for every internal operation) are no improvement on direct field access. They double the surface area while hiding nothing. The second is the value-object case: some objects (DTOs, plain records, schemas) are supposed to expose their fields; forcing accessors on them produces ceremony and noise. F6 naively applied produces JavaBean clutter or anaemic OO designs where every class is a shell of getters.
Counter-argument retort
The first head is right and is the operational refinement F6 needs. Encapsulation is not a count of accessors; it is the depth of behaviour behind an interface. A module with twenty getters and no behaviour has not encapsulated anything; it has built a protocol-buffer in OO clothes. The principle Ousterhout names, deep modules, is the goal F6 is in service of. Treat narrow interface, substantial body as the criterion, not private keyword on every field.
The second head is also true and is absorbed by F6, not refuted by it. Value objects, DTOs, schemas, and language-level record / data class constructs are precisely the cases where the public state is the contract. Use them; the modern languages have first-class support. F6 says public mutable state is a defect; immutable value-objects are the case the rule has the language-level escape hatch for.
Default stance for behaviour-bearing units: visibility declared, public state private, Tell-Don't-Ask, narrow interface, substantial body. Default stance for value-bearing units: readonly, immutable, all-public-by-design. Anything else is an exception that gets defended in the PR description.
Notes
- [1]David Parnas — On the Criteria to be Used in Decomposing Systems into Modules — CMU TR CMU-CS-71-101 (August 1971); Communications of the ACM 15(12), 1053–1058 (December 1972). The foundational paper. The KWIC index example is on pp. 6–14; the information-hiding criterion is in §7 'The Criteria'.
- [2]Barbara Liskov — Data Abstraction and Hierarchy — OOPSLA '87 keynote, addendum published in ACM SIGPLAN Notices 23(5), 17–34, May 1988. Formalised as the Liskov Substitution Principle by Liskov & Wing (1994).
- [3]Bertrand Meyer — Object-Oriented Software Construction (2nd ed., Prentice-Hall 1997). Design by Contract — preconditions, postconditions, class invariants — as the executable form of the encapsulated interface. Eiffel was the first language with first-class contract syntax.