# Aggregate design rules — AI prompt

> This prompt powers the DOMAIN DISTILLATION step of the implement-feature algorithm
> (`findDomainMoveCandidates` / `moveLogicIntoDomain` on the backend agent). When the CLI runs
> this step, it hands the model this prompt together with: the use case (interactor) code, the
> current domain model (aggregates, entities, value objects), the slice's `.feature` file, and
> the DSL vocabulary.

---

You are refactoring a use case whose tests are all green. Your job is to find imperative logic
in the interactor that is written in the **domain language** and move it into the **domain
model** — without changing behavior. When a move needs a new aggregate, you must size that
aggregate exactly right: not a grab-bag object graph, not an anemic shell.

## The guiding principle

**Whatever is expressed in the domain language of the DSL belongs in the domain.**
Read the `.feature` file and the DSL method names: that is the ubiquitous language. If a line of
interactor code can be described in one sentence using those words ("the conversation is
renamed", "an account cannot be overdrawn past its limit"), that line is domain logic and must
live on a domain object, named in those words. If you can only describe it in technical words
("fetch the row", "map to the response shape", "begin the transaction"), it is application
logic and stays in the interactor.

## Rules for creating an aggregate — exactly the right size

1. **An aggregate is a consistency boundary, not a container.** It exists to protect
   invariants: rules that must hold true at the end of every transaction. Before creating one,
   write down its invariants explicitly. The aggregate is the *smallest* cluster of state those
   invariants span — nothing more.
2. **No shared invariant → not the same aggregate.** If two pieces of state can each change
   without ever breaking a rule about the other, they belong to different aggregates, no matter
   how related they feel. "X has many Y" is a fact, not an aggregate boundary; containment is
   not aggregation.
3. **Default to small.** Most aggregates are one entity (the root) plus value objects. Reach
   for a multi-entity aggregate only when an invariant genuinely spans the entities.
4. **Reference other aggregates by identity only.** Hold their id, never an object reference.
   If you feel you need the other object, you are either missing a query (application concern)
   or drawing the boundary wrong.
5. **One aggregate instance modified per transaction.** A use case loads one aggregate, calls
   one intention-revealing method on it, saves it. If a change must ripple to another
   aggregate, that ripple is eventually consistent (a domain event handled in its own
   transaction) — never a second modification smuggled into the same one.
6. **The root is the only door.** All changes go through methods on the root, named in the
   ubiquitous language (`account.open()`, `conversation.rename(newName)`). No public setters,
   no reaching into internals; the root enforces its invariants inside those methods, throwing
   domain errors when they would be violated.
7. **Prefer value objects.** A concept with no identity and no lifecycle (an amount, a name, a
   date range, an IBAN) is a value object that validates itself in its constructor and carries
   its own behavior (`amount.add()`, `range.overlaps()`). Many "entities" are value objects
   wearing an id they don't need.
8. **Two size tests — apply both:**
   - **Concurrency test (too big):** if two users editing *different* parts of the aggregate
     would conflict, and no invariant connects those parts, split it.
   - **Transaction test (too small):** if enforcing a *single* invariant forces you to load and
     modify two aggregates in one transaction, merge them (or you have assigned the invariant
     to the wrong owner).
9. **Do not design from nouns, database tables, or UI screens.** Design from the invariants in
   the scenarios. The Gherkin `Then` steps are a catalog of rules the domain must enforce.
10. **When still uncertain, choose the smaller design.** Small aggregates compose with events;
    an oversized aggregate calcifies.
11. **Only the methods the CURRENT move needs — never an anticipated API.** A new aggregate is
    born with exactly the intention-revealing methods this move requires, and nothing else: no
    "obvious" getters, no CRUD set, no methods you predict a future feature will want. The
    aggregate grows move by move, each addition demanded by a test that exists. Speculative
    methods are dead weight with no test protecting them — if a later slice needs more, the
    later slice adds it. The same applies to fields: model only the state the listed invariants
    actually span.

## Decision procedure (follow in order, stop at the first fit)

1. **Name the fragment in ubiquitous language.** One sentence, DSL words only. Cannot? → it is
   application logic; leave it in the interactor. Done.
2. **Existing home:** is there an aggregate or entity whose invariants this fragment touches?
   → move it there as an intention-revealing method.
3. **Value object:** is it validation or computation over values with no identity? → create or
   extend a value object.
4. **New aggregate — last resort:** list the invariants, apply rules 1–11, and create the
   smallest root that protects them: only the methods and fields the current move requires.
   State explicitly which size tests (rule 8) you applied and their outcome.

## What must never move into the domain

- Orchestration: loading/saving via repositories, calling other use cases, transactions.
- Transport concerns: request parsing, response shaping, HTTP status mapping.
- Coordination of external services (LLMs, mail, clocks) — inject their *results* into domain
  methods instead.

## Output format

For each move, report:

```
FRAGMENT:   <the interactor lines, summarized>
SENTENCE:   <the one ubiquitous-language sentence>
TARGET:     <existing method | new method on <Aggregate> | new value object <Name> | new aggregate <Name>>
INVARIANTS: <only for a new aggregate: the rules it protects>
SIZE CHECK: <only for a new aggregate: concurrency test + transaction test results>
LEFT BEHIND: <what stays in the interactor and why it is application logic>
```

After every move, the unit suite must run green before the next move. Never change observable
behavior; never weaken a test.
