Object-Oriented Design
Overview
The pack is design rules for codebases written in an object-oriented style, not universal advice. A project's own written conventions win over the pack. Follow the idiom of the module being changed. This skill sizes the task and names the skill that fits.
Load the skill this index names before you answer: its rules and examples live there, not here.
How much design a task warrants
| Task | Use | Skip |
|---|---|---|
| An answer used once, then thrown away | Nothing | Everything |
| One method in a class that exists | Its name and its contract | Remodelling |
| A feature inside an existing flow | The idiom that flow uses | New boundaries |
| A new class or module | The design loop | Nothing |
| A new context | The loop, then its boundaries | Nothing |
| A new system from nothing | The loop, contexts, then structure | Nothing |
Before designing
Before designing, state the gate: one rule the software must follow, with one example it accepts and one it rejects. For example, "A slot can hold at most one appointment." It accepts booking a second patient into a different slot and rejects booking a second patient into the same slot. If you can write the rule down, start. If you cannot, what is missing is not design, and this pack cannot supply it.
The design loop
- Say what the software must do in the domain's words (speaking the domain language), and find the object that knows what each behaviour needs (assigning responsibilities).
- Find the consistency boundary: what must be true together in one transaction (designing aggregates).
- Decide where each rule lives: an entity, a value object, or a domain service (placing domain logic).
- Design the messages each object answers (encapsulating state and behaviour), and make illegal states impossible to construct (making illegal states impossible).
- Invert dependencies so the domain knows nothing of the database or the framework (inverting dependencies, layering the system).
- Name things in the domain's words, and comment only what the code cannot say (naming classes and methods, commenting what code cannot say).
- Check the result against the red flags below (reviewing object design).
How strictly a rule is meant
Every core rule in this pack carries one of three labels, and the label says how strictly it is meant.
- Rule. Breaking it is a defect. No framework excuses it.
- Default. Do this unless a stated condition holds. The condition is its own sentence, never a trailing clause.
- Judgement. Two answers are defensible. The skill says what tips the balance.
Symptoms to skills
Symptoms point to the skill that treats them, and this table grows as skills land.
| Symptom | Skill |
|---|---|
| Which class should do this? | Assigning responsibilities |
| This class does too much | Assigning responsibilities |
| A change ripples through many classes | Diagnosing coupling |
| How tightly are these two parts tied? | Diagnosing coupling |
| Is this an object or just data? | Deciding object or data |
| Our domain model is anemic | Deciding object or data |
| Other code decides for this object | Encapsulating state and behaviour |
| Setters let this object break its rules | Encapsulating state and behaviour |
| This object can exist half-built | Constructing valid objects |
| This constructor takes too many arguments | Constructing valid objects |
| Every new case means editing working code | Encapsulating what varies |
| How much flexibility should this have? | Encapsulating what varies |
| Is this a value or an entity? | Modeling values and entities |
| Raw strings and numbers carry domain rules | Modeling values and entities |
| Should this extend that class or hold it? | Choosing composition over inheritance |
| Changing a base class breaks its subclasses | Choosing composition over inheritance |
| A subclass breaks what its parent promises | Honouring substitutability |
| Code checks which subtype it holds | Honouring substitutability |
| The same switch on a type keeps coming back | Replacing conditionals with polymorphism |
| Subclasses or pattern matching? | Replacing conditionals with polymorphism |
| Code reaches through a chain of objects | Limiting what an object knows |
| This class only forwards calls | Limiting what an object knows |
| What should this class or method be called? | Naming classes and methods |
| Names like Manager, Helper or Util | Naming classes and methods |
| Callers depend on interface methods they never use | Designing interfaces |
| Interface or abstract base class? | Designing interfaces |
| Domain code depends on a database or framework | Inverting dependencies |
| Where are objects created and wired together? | Inverting dependencies |
| Callers know how another class stores or formats data | Hiding implementation |
| A class leaks its internal errors or steps | Hiding implementation |
| Flags or optional fields allow impossible combinations | Making illegal states impossible |
| A type admits values the domain never has | Making illegal states impossible |
| The code's words differ from the staff's | Speaking the domain language |
| One word means different things to two teams | Speaking the domain language |
| Which objects must change together? | Designing aggregates |
| Saves collide or one load pulls in too much | Designing aggregates |
| Which status changes are allowed? | Modeling lifecycles |
| A cancelled or closed object changes again | Modeling lifecycles |
| How should domain objects be stored and found? | Designing repositories |
| The repository has a find method per screen | Designing repositories |
| Use cases repeat the same business check | Placing domain logic |
| Domain service, entity method or application service? | Placing domain logic |
| Other parts must react after a change is saved | Publishing domain events |
| A message went out for a change that rolled back | Publishing domain events |
| Where does one team's model end and another's begin? | Bounding contexts |
| Which parts are core, and which can we buy? | Bounding contexts |
| Which way should the layers depend? | Layering the system |
| A screen or job cannot be tested without its framework | Layering the system |
| Modules depend on each other in a cycle | Organising modules |
| Modular monolith or separate services? | Organising modules |
| A change to our API breaks the apps that call it | Crossing system boundaries |
| How should we version this API or message? | Crossing system boundaries |
| The mapping tool shapes our domain classes | Taming persistence |
| A list runs one query per row | Taming persistence |
| Screens need data shaped unlike the model | Separating reads from writes |
| Read model, projection or event sourcing? | Separating reads from writes |
| MVC, MVP or MVVM for this screen? | Separating presentation from domain |
| A screen edits domain objects directly | Separating presentation from domain |
| Which design pattern fits this problem? | Choosing a pattern |
| A pattern was added before its problem | Choosing a pattern |
| These pattern classes could be a function | Patterns the language provides |
| Visitor, or a closed set with matching? | Patterns the language provides |
| A method returns null when it finds nothing | Designing failure behaviour |
| Refusals come back as codes callers ignore | Designing failure behaviour |
| Threads share an object and lose or mix its changes | Managing shared state |
| A lock or a single owner for this state? | Managing shared state |
| A call to another system hangs or fails midway | Making work reliable |
| A retry did the work twice | Making work reliable |
| We cannot explain an incident from our records | Designing what to record |
| What should we log, count or audit? | Designing what to record |
| A review found a god class or lava flow | Recognising antipatterns |
| Which antipattern should we fix first? | Recognising antipatterns |
| Tests break although behaviour did not change | Testing object-oriented code |
| Mock, stub or fake for this collaborator? | Testing object-oriented code |
| Tests depend on the time or random values | Designing for testability |
| Code is made public only so tests can reach it | Designing for testability |
| How do we restructure this code safely? | Refactoring toward object design |
| A refactoring grew too big to finish | Refactoring toward object design |
| We must change code that has no tests | Working with legacy code |
| Replace an old system piece by piece | Working with legacy code |
| What should this review check? | Reviewing object design |
| Which review findings block a merge? | Reviewing object design |
| Nobody remembers why the code is shaped this way | Documenting design decisions |
| What should we draw, and at what level? | Documenting design decisions |
| Comments repeat the code or contradict it | Commenting what code cannot say |
| Where do we explain why this code looks odd? | Commenting what code cannot say |
| A framework hook or marker holds a business rule | Working within a framework |
| How far should we go along with the framework? | Working within a framework |
| A calculation also saves, sends or changes its inputs | Applying functional techniques |
| Which functional techniques fit object code? | Applying functional techniques |
Language skills
A language skill keeps the core rules in one language and never replaces them. A base language skill is for code with no framework.
oo-java, fromobject-oriented-design-java: records, sealed types,equalsandhashCode, checked exceptions,Optional, and classes closed to extension.oo-kotlin, fromobject-oriented-design-kotlin: data classes, sealed types, nullable types, delegation withby, and extension functions.oo-typescript, fromobject-oriented-design-typescript: branded identifiers,#privatestate, abstract classes, decorators, discriminated unions, and a composition root.oo-javascript, fromobject-oriented-design-javascript: contract tests,#privatestate, frozen values, and classes rather than edited prototypes.oo-csharp, fromobject-oriented-design-csharp: records whose copies keep their checks, structs, nullable reference types, default interface members, andasynckept in the shell.oo-python, fromobject-oriented-design-python: protocols and abstract base classes, frozen dataclasses, properties without setters, mixins and the method resolution order, andfinal.oo-ruby, fromobject-oriented-design-ruby: contract tests, messages instead of class checks, readers without writers, values withData, held objects instead of mixins, and methods defined where a reader finds them.- A language with no skill of its own reads the nearest shipped one:
Scala and Groovy read
oo-java, and PHP readsoo-python.
In Claude Code, install a language plugin beside the core one:
/plugin install object-oriented-design-java@object-oriented-design-skills
Quick reference
Common design questions, each with the answer this pack gives and the rule it rests on.
| Question | Answer | Skill | Rule |
|---|---|---|---|
| How many reasons to change per class? | One kind of change request. | Assigning responsibilities | 7 |
| Command/query? | Writes, no return; reads, no change. | Encapsulating state and behaviour | 4 |
| Return its list? | No; methods or a read-only view. | Encapsulating state and behaviour | 5 |
| How do I keep a value from changing? | Make it immutable. | Modeling values and entities | 2 |
| Should this class allow extension? | No, unless built for it. | Honouring substitutability | 5 |
| Should an aggregate hold another aggregate? | No; hold its identity. | Designing aggregates | 3 |
| Where should input checks live? | At the edge; domain decides. | Placing domain logic | 5 |
| How much should a module expose? | A small public interface. | Organising modules | 6 |
| How do outside types reach the domain? | Map at the boundary. | Crossing system boundaries | 2 |
| Where should repository interfaces go? | In the domain, types only. | Designing repositories | 2 |
| How do I name a test? | For the behaviour it checks. | Testing object-oriented code | 3 |
| When should preconditions be checked? | Before any change. | Designing failure behaviour | 4 |
Red flags
Places to look, not verdicts. A red flag becomes a finding only with evidence: a measurement, a reproducible bug, or a failing test.
- A class named Manager, Helper or Util.
- A method that asks another object for its data and then decides for it.
- A constructor that leaves the object unusable until a setter runs.
- One business rule whose change touches many classes.
- A test that needs many mocks to check one decision.