docs(skills): prune guidance and route conditional references (R11)
This commit is contained in:
1 parent
9ada035382
commit
521d57f09d
11 files changed
+255
-2102
No files matched your search
@@ -7,190 +7,29 @@ metadata:
|
||||
version: "1.4.0"
|
||||
---
|
||||
|
||||
# Domain-Driven Design Framework
|
||||
# Domain-Driven Design
|
||||
|
||||
Framework for tackling software complexity by modeling code around the business domain. The greatest risk in software is not technical failure -- it is building a model that does not reflect how the business actually works.
|
||||
Model the requested business behavior, not an idealized architecture. Start with the user's scenarios, existing code, root `CONTEXT.md`, and relevant accepted ADRs.
|
||||
|
||||
Apply [the repository's development data lifecycle](../../../AGENTS.md#development-data-lifecycle) before using any compatibility pattern below: replace obsolete development models and update all callers and tests together. Examples involving legacy systems concern genuine external constraints, not a reason to retain this application's obsolete schemas.
|
||||
## Shared constraints
|
||||
|
||||
## Core Principle
|
||||
- Preserve the repository's language and existing architectural decisions. Surface conflicts with evidence rather than silently replacing them.
|
||||
- Follow [the development data lifecycle](../../../AGENTS.md#development-data-lifecycle): replace obsolete development schemas/contracts and update callers and tests together. Compatibility patterns apply only to genuine external constraints or a later explicit production-data decision.
|
||||
- Choose aggregate boundaries from invariants and ownership. Preserve [the accepted rental transaction design](../../../docs/adr/0011-rental-aggregate-boundaries.md), including atomic operations across aggregate documents.
|
||||
- Add events, factories, translation layers, or separate contexts only for a current business or integration need. Simple CRUD is valid; a score or wider architecture audit is optional, not a completion gate.
|
||||
- Honor the requested deliverable: a design or review does not itself authorize implementation, migrations, or external changes.
|
||||
|
||||
**The model is the code; the code is the model.** Software should embody a deep, shared understanding of the business domain. When domain experts and developers speak the same language and that language is directly expressed in the codebase, complexity becomes manageable and the system evolves gracefully as the business changes.
|
||||
## Use the relevant reference
|
||||
|
||||
## Evaluation
|
||||
Read only the branch needed by the task:
|
||||
|
||||
Evaluate the requested model against its business invariants, known callers, and accepted ADRs. Use the diagnostic questions as prompts, not a mandatory architecture scorecard. Score only if requested, explaining the criteria and limitations; simple CRUD does not need events, factories, or translation layers to earn approval. Preserve [the accepted rental aggregate transaction design](../../../docs/adr/0011-rental-aggregate-boundaries.md).
|
||||
- Resolving ambiguous terms or maintaining a glossary: [ubiquitous language](references/ubiquitous-language.md).
|
||||
- Choosing context boundaries or mapping integrations: [bounded contexts](references/bounded-contexts.md).
|
||||
- Deciding identity, value semantics, aggregate ownership, or atomicity: [building blocks](references/building-blocks.md).
|
||||
- Designing a real event flow or evaluating event sourcing: [domain events](references/domain-events.md).
|
||||
- Defining persistence contracts, reconstruction, or complex creation: [repositories and factories](references/repositories-factories.md).
|
||||
- Prioritizing modeling effort or comparing build/buy choices: [strategic design](references/strategic-design.md).
|
||||
|
||||
## Framework
|
||||
## Outcome
|
||||
|
||||
### 1. Ubiquitous Language
|
||||
|
||||
**Core concept:** A shared, rigorous language between developers and domain experts, used consistently in conversation, documentation, and code. When the language changes, the code changes -- and awkward naming in code feeds back into refining the language.
|
||||
|
||||
**Why it works:** Ambiguity is the root cause of most modeling failures. When a developer says "order" and an expert means "purchase request," bugs are inevitable; a ubiquitous language forces every name in code to map to a concept the business recognizes and validates.
|
||||
|
||||
**Key insights:**
|
||||
- The language emerges from deep collaboration, not a glossary bolted on after the fact
|
||||
- If a concept is hard to name, the model is likely wrong -- naming difficulty is a design signal
|
||||
- Technical jargon (`DataProcessor` vs. `ClaimAdjudicator`) hides domain logic from the experts who could correct it
|
||||
- Different bounded contexts may use the same word with different meanings -- and that is fine
|
||||
|
||||
**Code applications:**
|
||||
|
||||
| Context | Pattern | Example |
|
||||
|---------|---------|---------|
|
||||
| Class/method naming | Name after domain concepts and verbs | `LoanApplication`, `policy.underwrite()` -- not `RequestHandler`, `process()` |
|
||||
| Module structure | Organize by domain concept | `shipping/`, `billing/` -- not `controllers/`, `services/` |
|
||||
| Code review | Reject technical-only names | Flag `Manager`, `Helper`, `Processor`, `Utils` as naming smells |
|
||||
|
||||
See: [references/ubiquitous-language.md](references/ubiquitous-language.md) when running modeling sessions or maintaining a glossary -- covers how the language evolves and feeds back into code.
|
||||
|
||||
### 2. Bounded Contexts and Context Mapping
|
||||
|
||||
**Core concept:** A bounded context is an explicit boundary within which a particular domain model applies. The same word ("Customer") can mean different things in different contexts; context maps define the relationships and translation strategies between them.
|
||||
|
||||
Use separate contexts when language, rules, or ownership genuinely differ. A coherent single-context application does not need extra contexts or services merely to follow DDD.
|
||||
|
||||
**Key insights:**
|
||||
- A bounded context is not a microservice -- it is a linguistic and model boundary that may contain multiple services
|
||||
- Context boundaries often align with team boundaries (Conway's Law)
|
||||
- The nine context mapping patterns describe political and technical relationships between teams
|
||||
- Use a translation boundary when a foreign model would distort domain rules; avoid a pass-through layer when the existing contract already fits
|
||||
- Shared Kernel couples two teams; keep it small and explicitly governed
|
||||
- Start by mapping what exists (Big Ball of Mud), then define target boundaries
|
||||
|
||||
**Code applications:**
|
||||
|
||||
| Context | Pattern | Example |
|
||||
|---------|---------|---------|
|
||||
| Service integration | Anti-Corruption Layer | Translate external API responses into your domain objects at the boundary |
|
||||
| Legacy migration | Conformist / ACL | Wrap the legacy system behind an adapter that speaks your domain language |
|
||||
| API design | Open Host Service + Published Language | Expose a well-documented REST API with a canonical schema |
|
||||
|
||||
See: [references/bounded-contexts.md](references/bounded-contexts.md) for the nine mapping patterns and integration strategies.
|
||||
|
||||
### 3. Entities, Value Objects, and Aggregates
|
||||
|
||||
**Core concept:** Entities have identity that persists across state changes. Value Objects are defined entirely by their attributes and are immutable. Aggregates are clusters of entities and value objects with a single root that enforces consistency boundaries.
|
||||
|
||||
These distinctions clarify identity and invariant ownership. Choose consistency across aggregates from the actual business requirement and storage guarantees, not a universal eventual-consistency rule.
|
||||
|
||||
**Key insights:**
|
||||
- Entity test: "Am I the same thing even if all my attributes change?" (a person changes name and address -- still the same person)
|
||||
- Value Object test: "Am I defined only by my attributes?" (any $10 bill is interchangeable with another)
|
||||
- Most things should be Value Objects, not Entities -- prefer immutability
|
||||
- Keep aggregates small (one root plus a minimal cluster); reference other aggregates by ID, not object reference
|
||||
- Use cross-aggregate transactions when required for atomic business operations, as accepted in this repository; use eventual consistency only when temporary inconsistency is acceptable
|
||||
|
||||
**Code applications:**
|
||||
|
||||
| Context | Pattern | Example |
|
||||
|---------|---------|---------|
|
||||
| Identity tracking | Entity with ID | `Order` identified by `orderId`, survives state changes |
|
||||
| Immutable attributes | Value Object | `Address(street, city, zip)` -- replace, never mutate |
|
||||
| Consistency boundary | Aggregate Root | `Order` is root; `OrderLine` items exist only through it |
|
||||
| Concurrency control | Optimistic locking on root | Version field on `Order`; conflict if two edits race |
|
||||
|
||||
See: [references/building-blocks.md](references/building-blocks.md) for aggregate design rules and consistency boundaries.
|
||||
|
||||
### 4. Domain Events
|
||||
|
||||
**Core concept:** A domain event captures something that happened in the domain that experts care about, named in past tense (`OrderPlaced`, `PaymentReceived`) -- a fact that has already occurred.
|
||||
|
||||
**Why it works:** Domain events decouple cause from effect. When `OrderPlaced` is published, shipping, billing, and notifications each react independently without the ordering context knowing about them -- less coupling, eventual consistency, a natural audit trail.
|
||||
|
||||
**Key insights:**
|
||||
- Events are immutable facts -- once published, they cannot be changed or retracted
|
||||
- Domain events are internal to a bounded context; integration events cross boundaries
|
||||
- Events enable temporal decoupling: the producer does not wait for the consumer
|
||||
- Event sourcing stores the full event history as the source of truth, deriving current state by replay
|
||||
- Not every state change deserves an event -- only publish what the domain cares about
|
||||
|
||||
**Code applications:**
|
||||
|
||||
| Context | Pattern | Example |
|
||||
|---------|---------|---------|
|
||||
| State transitions | Raise event on domain action | `order.place()` raises `OrderPlaced` |
|
||||
| Cross-context integration | Publish integration event | `OrderPlaced` triggers `ShippingLabelRequested` in shipping context |
|
||||
| Eventual consistency | Async event handlers | Inventory handler updates stock asynchronously after `OrderPlaced` |
|
||||
|
||||
See: [references/domain-events.md](references/domain-events.md) for event naming, event sourcing, and integration events.
|
||||
|
||||
### 5. Repositories and Factories
|
||||
|
||||
**Core concept:** Repositories provide the illusion of an in-memory collection of domain objects, hiding persistence. Factories encapsulate complex creation logic so aggregates are always born in a valid state.
|
||||
|
||||
**Why it works:** When persistence and assembly details leak into domain code, every storage change ripples through business rules and aggregates can be constructed in half-valid states. Repositories confine SQL/ORM concerns to infrastructure so the domain stays testable in memory; factories make the only path to an aggregate one that enforces its invariants, so an invalid instance is unrepresentable.
|
||||
|
||||
**Key insights:**
|
||||
- The Repository interface belongs in the domain layer; its implementation belongs in infrastructure
|
||||
- Repository methods speak the ubiquitous language: `findPendingOrders()`, not `getByStatusCode(3)`
|
||||
- Collection-oriented repositories mimic `add`/`remove`; persistence-oriented ones use `save`
|
||||
- Factories are warranted for complex rules or multi-part assembly; a two-field Value Object just needs a constructor
|
||||
- The Specification pattern encapsulates query criteria as domain objects: `OverdueInvoiceSpecification`
|
||||
|
||||
**Code applications:**
|
||||
|
||||
| Context | Pattern | Example |
|
||||
|---------|---------|---------|
|
||||
| Data access abstraction | Repository interface | `OrderRepository.findByCustomer(customerId)` in domain; `PostgresOrderRepository` in infrastructure |
|
||||
| Complex creation | Factory method | `Order.createFromQuote(quote)` validates and assembles from a `Quote` aggregate |
|
||||
| Query encapsulation | Specification | `spec = OverdueBy(days=30); repo.findMatching(spec)` |
|
||||
|
||||
See: [references/repositories-factories.md](references/repositories-factories.md) for Repository, Factory, and Specification patterns.
|
||||
|
||||
### 6. Strategic Design and Distillation
|
||||
|
||||
**Core concept:** Not all parts of a system are equally important. Strategic design identifies the Core Domain -- where competitive advantage lives -- and distinguishes it from Supporting Subdomains (necessary, not differentiating) and Generic Subdomains (commodity).
|
||||
|
||||
**Why it works:** Applying the same rigor everywhere spreads your best talent thin and over-engineers commodity functionality. Identifying the Core Domain concentrates the best developers and deepest modeling where they matter most.
|
||||
|
||||
**Key insights:**
|
||||
- Core Domain: invest your best people and deepest modeling; Supporting: build, but don't over-engineer; Generic (auth, email, payments): buy or use open-source
|
||||
- Distillation extracts and highlights the Core Domain from surrounding complexity
|
||||
- A Domain Vision Statement is a one-page description of the Core Domain's value proposition
|
||||
- Revisit what is "core" as the business evolves -- today's differentiator may become tomorrow's commodity
|
||||
|
||||
**Code applications:**
|
||||
|
||||
| Context | Pattern | Example |
|
||||
|---------|---------|---------|
|
||||
| Build vs. buy | Classify subdomain type | Build custom pricing engine (core); use Stripe for payments (generic) |
|
||||
| Team allocation | Best developers on Core Domain | Seniors model underwriting rules; juniors integrate the email service |
|
||||
| Code organization | Separate core from generic | `domain/pricing/` (deep model) vs. `infrastructure/email/` (thin adapter) |
|
||||
|
||||
See: [references/strategic-design.md](references/strategic-design.md) when deciding where to invest engineering effort -- subdomain classification and distillation techniques.
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| Mistake | Why It Fails | Fix |
|
||||
|---------|-------------|-----|
|
||||
| Technical names instead of domain language | Logic hidden behind `DataManager`; experts can't validate the model | Rename to domain terms (`ClaimAdjudicator`); if no domain term exists, the concept may be wrong |
|
||||
| One model to rule them all | A single `Customer` class for billing, shipping, and marketing becomes bloated and contradictory | Bounded contexts: each gets its own `Customer` with only the attributes it needs |
|
||||
| Giant aggregates | Concurrency conflicts, slow loads, transactional bottlenecks | Keep cohesive aggregates small and references explicit while preserving required atomicity |
|
||||
| Anemic domain model | Objects are data bags; rules scatter across services and duplicate | Move behavior into entities and value objects; services orchestrate only |
|
||||
| Foreign model leaking into domain rules | Business behavior depends on external representation | Translate at the affected boundary when needed; avoid unnecessary pass-through layers |
|
||||
| Bounded context = microservice | Premature extraction; distributed complexity without benefit | A context is a model boundary, not a deployment unit; start with modules in a monolith |
|
||||
| Skipping domain experts | Developers invent a model that doesn't match reality; expensive rework | Regular modeling sessions until experts say "yes, that is how it works" |
|
||||
|
||||
## Quick Diagnostic
|
||||
|
||||
| Question | If No | Action |
|
||||
|----------|-------|--------|
|
||||
| Can a domain expert read your class names and understand them? | Technical jargon hides the model | Rename classes, methods, events to ubiquitous language |
|
||||
| Are bounded context boundaries explicitly defined? | Models bleed; same term means different things | Draw a context map; define boundaries and translations |
|
||||
| Are aggregates cohesive and bounded? | Slow loads or contention may result | Reconsider ownership while preserving business atomicity |
|
||||
| Do domain objects contain behavior, not just data? | Anemic model; logic scattered in services | Move business rules into entities and value objects |
|
||||
| Does cross-aggregate coordination preserve required consistency? | Partial changes may violate business rules | Choose transactions or asynchronous events according to the invariant and accepted ADRs |
|
||||
| Are external representations isolated where their semantics differ? | Foreign models can distort domain rules | Translate where needed; a separate layer is not mandatory for every integration |
|
||||
| Have you identified which subdomain is core? | Best talent spread thin | Classify subdomains; focus deep modeling on the Core Domain |
|
||||
|
||||
## Further Reading
|
||||
|
||||
For the complete methodology, patterns, and deeper insights:
|
||||
|
||||
- [*"Domain-Driven Design: Tackling Complexity in the Heart of Software"*](https://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215?tag=wondelai00-20) by Eric Evans
|
||||
|
||||
## About the Author
|
||||
|
||||
**Eric Evans** is a software design consultant and the originator of Domain-Driven Design, developed through work on large-scale systems in finance, insurance, and logistics. His 2003 book *Domain-Driven Design: Tackling Complexity in the Heart of Software* is one of the most influential software architecture books ever written, and he continues to evolve DDD through his consultancy, Domain Language.
|
||||
Explain the proposed concepts, invariant owners, important transitions, and how the design handles failure and concurrency. Show only the alternatives that matter, identify unresolved domain questions, and propose checks that would falsify the design. When implementation is requested, update affected callers, tests, and domain documentation together.
|
||||
@@ -1,205 +1,32 @@
|
||||
# Bounded Contexts and Context Mapping
|
||||
|
||||
A bounded context is the most important strategic pattern in Domain-Driven Design. It defines an explicit boundary within which a particular domain model is defined, consistent, and applicable. Context mapping describes the relationships between bounded contexts and the strategies for translating between them.
|
||||
A bounded context defines where a domain model and its language apply. It is not automatically a microservice, package, or team-sized deployment.
|
||||
|
||||
## What Is a Bounded Context?
|
||||
## Find a real boundary
|
||||
|
||||
A bounded context is not a module, a microservice, or a deployment unit -- though it may coincide with any of these. It is a linguistic boundary: within this boundary, every term has a single, precise meaning, and the model is internally consistent.
|
||||
Look for differing business meanings, rules, ownership, or change lifecycles. Check concrete scenarios and existing ADRs. A single context may be correct; excessive translation between closely related concepts can signal an unnecessary split.
|
||||
|
||||
### The Problem It Solves
|
||||
For a requested context map, show the relevant contexts, ownership, upstream/downstream relationships, shared contracts, and any translation. Avoid mapping unrelated systems just to complete a catalog.
|
||||
|
||||
In any system of sufficient size, the same word means different things to different people:
|
||||
## Choose the relationship
|
||||
|
||||
| Term | In Sales Context | In Shipping Context | In Billing Context |
|
||||
|------|-----------------|--------------------|--------------------|
|
||||
| Customer | A prospect or account with contact info and purchase history | A delivery address with receiving instructions | A billing entity with payment methods and credit terms |
|
||||
| Product | A catalog item with descriptions, images, and pricing tiers | A physical item with weight, dimensions, and handling requirements | A line item with a price, tax category, and discount rules |
|
||||
| Order | A quote or deal being negotiated | A shipment to be picked, packed, and dispatched | An invoice to be generated and collected |
|
||||
| Pattern | Useful when | Cost to make explicit |
|
||||
| -------------------------------------- | ------------------------------------------------------------------ | -------------------------------------------------- |
|
||||
| Shared kernel | Closely collaborating owners genuinely share a small model | Coordinated changes and tests |
|
||||
| Customer–supplier | The upstream team can accommodate downstream needs | Planning and contract dependency |
|
||||
| Conformist | Accepting the upstream model is cheaper and safe for this use case | Coupling to upstream semantics |
|
||||
| Anti-corruption layer | Foreign semantics would distort local business rules | Translation and its tests |
|
||||
| Open host service / published language | Several consumers need a documented shared contract | Contract ownership and actual external obligations |
|
||||
| Separate ways | Integration costs more than limited duplication | Separate implementations and knowledge |
|
||||
| Partnership | Models must evolve together | Ongoing coordination |
|
||||
| Big ball of mud | Existing entanglement needs to be named during an assessment | A diagnosis, not a target design |
|
||||
|
||||
Trying to create a single `Customer` class that serves all three contexts produces a bloated, contradictory monstrosity with dozens of fields, most of which are irrelevant in any given use case. The bounded context pattern says: stop fighting this. Let each context have its own `Customer` model, optimized for its own needs.
|
||||
Translation can live in an existing adapter or mapper; it does not require a facade/adapter/translator class hierarchy. Validate untrusted external data even when adopting the upstream model directly.
|
||||
|
||||
### Identifying Bounded Context Boundaries
|
||||
## Changes and external constraints
|
||||
|
||||
Boundaries emerge from several signals:
|
||||
Preserve actual external contracts that the task cannot replace. Legacy coexistence patterns apply to a genuinely external system that must remain available, not to this application's disposable development model.
|
||||
|
||||
**Linguistic signals:**
|
||||
- When the same term means different things to different groups, those groups are in different contexts
|
||||
- When conversations between groups require "translation" ("when you say X, do you mean Y?"), there is a boundary
|
||||
- When a concept exists in one group but has no analog in another, the boundary is clear
|
||||
For internal model changes, follow [the development lifecycle](../../../../AGENTS.md#development-data-lifecycle): replace obsolete schemas/contracts, update callers and tests together, and reseed rather than introducing compatibility paths.
|
||||
|
||||
**Organizational signals:**
|
||||
- Different teams owning different parts of the system
|
||||
- Different departments with different processes and vocabulary
|
||||
- Different regulatory requirements (e.g., PCI compliance for payments vs. HIPAA for patient data)
|
||||
|
||||
**Technical signals:**
|
||||
- Different data storage needs (relational vs. document vs. event store)
|
||||
- Different consistency requirements (strong consistency for payments vs. eventual consistency for recommendations)
|
||||
- Different rate of change (billing rules change quarterly; product catalog changes daily)
|
||||
|
||||
### Context Size
|
||||
|
||||
There is no formula for the right size of a bounded context. However, guidelines help:
|
||||
|
||||
- **Too large:** If a single context contains concepts that do not cohesively relate, it is too large. A context that contains both "insurance underwriting" and "marketing campaign management" is probably two contexts.
|
||||
- **Too small:** If you find yourself creating translation layers between closely related concepts that change together and are owned by the same team, you may have split too aggressively.
|
||||
- **Rule of thumb:** A bounded context should be ownable by a single team (5-9 people). If a context requires multiple teams to modify, it is either too large or the team boundaries are wrong.
|
||||
|
||||
## Context Mapping Patterns
|
||||
|
||||
Context mapping describes how bounded contexts relate to each other. Eric Evans and the DDD community have identified nine primary patterns. Each represents a different political and technical relationship.
|
||||
|
||||
### 1. Shared Kernel
|
||||
|
||||
**What it is:** Two bounded contexts share a small subset of the model, typically a library of common types. Both teams co-own this shared code and must coordinate changes.
|
||||
|
||||
**When to use:** When two closely collaborating teams need the same domain concept (e.g., a `Money` value object or a `DateRange` type) and the overhead of translation is not justified.
|
||||
|
||||
**Risks:** Coupling. Any change to the shared kernel requires coordination between both teams. If not carefully managed, the shared kernel grows uncontrollably.
|
||||
|
||||
**Rules:**
|
||||
- Keep the shared kernel as small as possible -- value objects and basic types only
|
||||
- Require explicit agreement (both teams) for any change
|
||||
- Automated tests in both contexts must pass before any shared kernel change is merged
|
||||
- Never put entities or aggregates in the shared kernel
|
||||
|
||||
**Example:** Two contexts (billing and shipping) share a `Money(amount, currency)` value object and an `Address(street, city, state, zip, country)` value object.
|
||||
|
||||
### 2. Customer-Supplier
|
||||
|
||||
**What it is:** An upstream context (supplier) provides data or services that a downstream context (customer) depends on. The upstream team plans with the downstream team's needs in mind but has its own priorities.
|
||||
|
||||
**When to use:** When one team produces something another team consumes, and there is a reasonable working relationship. The downstream team can influence the upstream team's roadmap but does not control it.
|
||||
|
||||
**Example:** The Product Catalog team (upstream) provides product data to the Pricing team (downstream). The Pricing team requests new attributes when needed, and the Catalog team accommodates these requests in their planning.
|
||||
|
||||
### 3. Conformist
|
||||
|
||||
**What it is:** The downstream context conforms to the upstream context's model without translation. The downstream team accepts the upstream model as-is, even if it is not ideal.
|
||||
|
||||
**When to use:** When the upstream team has no incentive or ability to accommodate the downstream team (e.g., a large external service, a legacy system, or a dominant upstream team), and the cost of building a translation layer exceeds the cost of conforming.
|
||||
|
||||
**Risks:** Your model is constrained by someone else's design decisions. If the upstream model changes, your context must change too.
|
||||
|
||||
**Example:** A startup integrating with a major ERP system may conform to the ERP's data model rather than building translation layers, accepting the ERP's concept of "customer" and "order" even if they do not perfectly fit.
|
||||
|
||||
### 4. Anti-Corruption Layer (ACL)
|
||||
|
||||
**What it is:** A translation layer that sits between the downstream context and the upstream context, converting the upstream model into the downstream context's own domain language. The ACL protects the downstream model from being polluted by foreign concepts.
|
||||
|
||||
**When to use:** When you need to integrate with a system whose model does not fit yours, and you cannot afford to let that foreign model leak into your domain. This is the most important defensive pattern in DDD.
|
||||
|
||||
**Structure:**
|
||||
```
|
||||
Your Domain Layer <--> ACL (Adapters + Translators) <--> External System
|
||||
```
|
||||
|
||||
The ACL contains:
|
||||
- **Adapters** that handle the technical protocol (HTTP, gRPC, message queue)
|
||||
- **Translators** that convert external model objects into your domain objects
|
||||
- **Facades** that present a clean interface to your domain layer
|
||||
|
||||
**Example:** Integrating with a legacy mainframe that represents customers as `CUST_REC` with fields like `CUST_NM`, `CUST_ADDR1`. The ACL translates this into your domain's `Customer(name: PersonName, address: Address)` value objects.
|
||||
|
||||
### 5. Open Host Service (OHS)
|
||||
|
||||
**What it is:** An upstream context exposes a well-defined, documented protocol (API, message format) that downstream contexts can integrate with. The upstream team provides a stable, versioned interface designed for general consumption.
|
||||
|
||||
**When to use:** When an upstream context serves multiple downstream consumers and cannot tailor its interface to each one. The OHS provides a standard integration point.
|
||||
|
||||
**Characteristics:**
|
||||
- Explicit contracts for actual external consumers; add versioning or compatibility guarantees only where required, not for this application's disposable development contracts
|
||||
- Documentation and contracts (OpenAPI, protobuf schemas, JSON Schema)
|
||||
- The interface is deliberately designed for external consumption, not a direct exposure of the internal model
|
||||
|
||||
**Example:** An Identity Provider exposes an OAuth 2.0 / OpenID Connect API. Any downstream context can integrate using the standard protocol without knowing the internal user model.
|
||||
|
||||
### 6. Published Language
|
||||
|
||||
**What it is:** A well-documented, shared language (schema, format, protocol) used for communication between contexts. Often paired with Open Host Service.
|
||||
|
||||
**When to use:** When multiple contexts need to exchange data and a standard format prevents each integration from inventing its own.
|
||||
|
||||
**Examples:**
|
||||
- Industry-standard formats: HL7 for healthcare, SWIFT for banking, EDI for supply chain
|
||||
- Internal schemas: a company-wide JSON schema for events published on a shared message bus
|
||||
- Protocol Buffers or Avro schemas for service-to-service communication
|
||||
|
||||
### 7. Separate Ways
|
||||
|
||||
**What it is:** Two contexts decide not to integrate at all. Each builds its own solution independently, even if there is some overlap.
|
||||
|
||||
**When to use:** When the cost of integration (coordination, translation, coupling) exceeds the cost of duplication. Sometimes it is cheaper and simpler for two teams to build their own `Address` validation than to share one.
|
||||
|
||||
**Signals that Separate Ways is appropriate:**
|
||||
- The integration would be trivial functionality on both sides
|
||||
- The teams are in different organizations with different release cycles
|
||||
- The shared functionality is not core to either context
|
||||
|
||||
### 8. Big Ball of Mud
|
||||
|
||||
**What it is:** A system with no clear boundaries, where models are entangled and concepts leak everywhere. This is not a recommended pattern -- it is a recognition of reality. Many existing systems are Big Balls of Mud.
|
||||
|
||||
**When to use (as a label):** When mapping an existing landscape, some systems simply are Big Balls of Mud. Acknowledging this is the first step toward improvement. Drawing a boundary around the mud and treating it as a single (messy) context allows you to build clean contexts alongside it, protected by an ACL.
|
||||
|
||||
**Coexistence strategy for a genuinely external system that must remain in service:**
|
||||
1. Draw a boundary around the entire mudball
|
||||
2. Build new functionality in a clean bounded context
|
||||
3. Protect the new context with an ACL against the mudball
|
||||
4. Gradually extract functionality from the mud into clean contexts
|
||||
|
||||
For this repository's own obsolete development model, replace it directly and update callers/tests together instead of introducing this transitional architecture.
|
||||
|
||||
### 9. Partnership
|
||||
|
||||
**What it is:** Two contexts evolve together with mutual coordination. Both teams jointly plan features and synchronize releases. Neither dominates.
|
||||
|
||||
**When to use:** When two contexts are tightly coupled in the domain and both teams are committed to evolving together. More intimate than Customer-Supplier; both teams have equal say.
|
||||
|
||||
**Risks:** Requires strong coordination discipline. If one team slips, both are affected.
|
||||
|
||||
**Example:** A checkout context and a payment processing context that must evolve in lockstep when payment methods change.
|
||||
|
||||
## Team Relationships and Context Boundaries
|
||||
|
||||
### Conway's Law in Practice
|
||||
|
||||
Conway's Law states that systems mirror the communication structures of the organizations that build them. In DDD, this is not a warning -- it is a design tool:
|
||||
|
||||
- **Align context boundaries with team boundaries.** If one team owns billing and another owns shipping, these should be separate bounded contexts.
|
||||
- **If two teams must share a context, expect friction.** Either split the context or merge the teams.
|
||||
- **Cross-team integration should happen at context boundaries,** using well-defined mapping patterns, not through shared code or databases.
|
||||
|
||||
### Choosing the Right Pattern
|
||||
|
||||
| Situation | Recommended Pattern |
|
||||
|-----------|-------------------|
|
||||
| Two teams with a good relationship and shared concepts | Shared Kernel (kept small) or Customer-Supplier |
|
||||
| Integrating with a system you do not control | Anti-Corruption Layer |
|
||||
| Exposing your context to many consumers | Open Host Service + Published Language |
|
||||
| Integrating with a hostile or unresponsive upstream | Conformist (if cost is low) or Separate Ways |
|
||||
| Legacy system with no clear model | Big Ball of Mud (label it) + ACL around it |
|
||||
| Two teams that must evolve in lockstep | Partnership |
|
||||
| Integration cost exceeds duplication cost | Separate Ways |
|
||||
|
||||
### Drawing a Context Map
|
||||
|
||||
A context map is a visual representation of all bounded contexts and their relationships. It should include:
|
||||
|
||||
1. **Every bounded context** drawn as a labeled box
|
||||
2. **The relationships between them** using the patterns above (arrows show upstream/downstream)
|
||||
3. **The translation mechanism** (ACL, OHS, Shared Kernel)
|
||||
4. **Team ownership** for each context
|
||||
5. **The Big Balls of Mud** explicitly labeled
|
||||
|
||||
The context map is a communication tool. It should be understandable by both developers and non-technical stakeholders. Keep it on a whiteboard or in a shared diagram that the team references and updates regularly.
|
||||
|
||||
### When Boundaries Change
|
||||
|
||||
Context boundaries are not permanent. They evolve as the business and team structure change:
|
||||
|
||||
- **Splitting:** A context grows too large for one team; split it along natural seams and introduce a mapping pattern between the new contexts.
|
||||
- **Merging:** Two small contexts owned by the same team with heavy inter-communication; merge them and eliminate the translation overhead.
|
||||
- **Reclassifying:** A context that was Generic Subdomain becomes Core Domain as business strategy shifts; increase investment and modeling rigor.
|
||||
|
||||
The key is to make boundaries explicit and intentional, so that when they need to change, the change is a deliberate design decision rather than an accidental drift.
|
||||
Explain a proposed split or merge through its effect on language, invariant ownership, coupling, and coordination. Do not reorganize teams, deploy services, or modify contracts merely because a map was requested.
|
||||
@@ -1,236 +1,35 @@
|
||||
# Building Blocks: Entities, Value Objects, and Aggregates
|
||||
# Entities, Value Objects, and Aggregates
|
||||
|
||||
The tactical building blocks of Domain-Driven Design provide a vocabulary for structuring domain models. Entities, Value Objects, and Aggregates are the three most critical patterns. Getting them right determines whether a domain model is expressive and maintainable or bloated and fragile.
|
||||
Start from identity and business invariants rather than database tables or a target class count.
|
||||
|
||||
## Entities
|
||||
## Identity and value
|
||||
|
||||
An Entity is a domain object defined by its identity rather than its attributes. An entity persists across time and state changes -- it is the "same thing" even when everything about it changes.
|
||||
An entity retains identity through changes; a value object is interchangeable with another value having the same attributes. Choose equality and mutability accordingly. Use existing identifier conventions rather than adding a new ID scheme by default.
|
||||
|
||||
### The Identity Test
|
||||
Value objects should enforce their actual invariants and support immutable transformations. Do not infer business rules from examples: negative money may be valid for one concept and invalid for another.
|
||||
|
||||
Ask: "If all the attributes change, is it still the same thing?"
|
||||
## Aggregate ownership
|
||||
|
||||
- A **person** changes name, address, phone number, job, and appearance -- still the same person. **Entity.**
|
||||
- A **bank account** changes its balance daily -- still the same account. **Entity.**
|
||||
- A **$10 bill** is interchangeable with any other $10 bill. **Not an entity -- Value Object.**
|
||||
An aggregate owns a cohesive set of state and invariants, with a root controlling its changes. Keep it small enough to load and update for its real use cases; use references by ID for independent aggregates where the project's model calls for them.
|
||||
|
||||
### Identity Strategies
|
||||
Ask:
|
||||
|
||||
| Strategy | How It Works | When to Use |
|
||||
|----------|-------------|-------------|
|
||||
| Natural key | Use a real-world identifier (SSN, ISBN, VIN) | When a stable, unique external identifier exists |
|
||||
| Surrogate key | Generate a synthetic ID (UUID, auto-increment) | When no natural key exists or the natural key can change |
|
||||
| Composite key | Combine multiple attributes | When identity is defined by a relationship (e.g., student + course = enrollment) |
|
||||
- Which rules must hold after every successful operation?
|
||||
- Who owns each lifecycle and may change its state?
|
||||
- Which state must be read and written together?
|
||||
- What prevents two concurrent commands from both accepting incompatible outcomes?
|
||||
- Can a partial result or delayed update be valid?
|
||||
|
||||
**Prefer UUIDs over auto-increment** for distributed systems. UUIDs can be generated anywhere without coordination; auto-increment requires a central authority.
|
||||
Do not embed a growing independent lifecycle merely to make all relationships reachable through one object.
|
||||
|
||||
### Entity Design Rules
|
||||
## Atomicity and concurrency
|
||||
|
||||
1. **Identity is immutable.** Once assigned, an entity's identity never changes. If the "identity" can change, it is not really the identity.
|
||||
2. **Entities are mutable.** Unlike Value Objects, entities change state over time. An `Order` moves from `Pending` to `Confirmed` to `Shipped`.
|
||||
3. **Equality is based on identity.** Two `Order` objects with the same `orderId` are the same order, regardless of other attribute differences.
|
||||
4. **Entities have a lifecycle.** They are created, go through state transitions, and may eventually be archived or deleted.
|
||||
5. **Put behavior on entities.** An entity is not a data container. `order.addItem(product, quantity)` belongs on the `Order` entity, not in an `OrderService`.
|
||||
Aggregate boundaries and transaction boundaries are related, not identical. When an operation must commit or roll back across multiple aggregate documents, use the supported service-layer transaction required by the domain.
|
||||
|
||||
### Common Entity Pitfalls
|
||||
In this repository, [ADR-0011](../../../../docs/adr/0011-rental-aggregate-boundaries.md) explicitly requires Mongo transactions for relevant rental operations. Preserve optimistic version checks, occupancy contention, and uniqueness constraints; replacing them with asynchronous events or a query-only overlap check would weaken the invariant.
|
||||
|
||||
- **Over-identification.** Making everything an entity when most things should be Value Objects. Ask the identity test for every class.
|
||||
- **Anemic entities.** Entities with only getters and setters. If all behavior is in services, the entity is a data bag.
|
||||
- **Identity leakage.** Exposing database primary keys as domain identity. Use domain-meaningful identifiers (`orderNumber`) rather than technical ones (`id: 42`).
|
||||
Use eventual consistency only when delayed or partial progress is acceptable, with explicit failure and recovery handling. Keep remote calls and long-running work outside database transactions.
|
||||
|
||||
## Value Objects
|
||||
## Verify the boundary
|
||||
|
||||
A Value Object is a domain object defined entirely by its attributes. It has no identity -- two Value Objects with the same attributes are interchangeable. Value Objects are immutable: you do not change a Value Object; you replace it.
|
||||
|
||||
### The Attribute Test
|
||||
|
||||
Ask: "Is it defined by what it is, not which one it is?"
|
||||
|
||||
- A **mailing address** (123 Main St, Springfield, IL 62704) -- defined by its attributes. Two objects with the same street, city, state, zip are the same address. **Value Object.**
|
||||
- A **money amount** ($49.99 USD) -- defined by amount and currency. **Value Object.**
|
||||
- A **date range** (Jan 1 - Dec 31) -- defined by start and end. **Value Object.**
|
||||
- A **color** (#FF5733) -- defined by its hex value. **Value Object.**
|
||||
|
||||
### Why Value Objects Matter
|
||||
|
||||
Value Objects are the unsung heroes of domain models. Most developers default to entities for everything, but **the majority of concepts in a well-designed domain model should be Value Objects.**
|
||||
|
||||
**Benefits of Value Objects:**
|
||||
|
||||
| Benefit | Explanation |
|
||||
|---------|-------------|
|
||||
| Immutability | No shared mutable state; safe to pass around, cache, and use in concurrent code |
|
||||
| Side-effect-free behavior | Methods return new Value Objects rather than mutating state; easy to reason about |
|
||||
| Self-validation | A Value Object validates itself on creation; an invalid Value Object can never exist |
|
||||
| Equality by value | `Money(100, "USD") == Money(100, "USD")` regardless of object reference |
|
||||
| Expressiveness | `Money` instead of `BigDecimal`; `EmailAddress` instead of `String`; domain meaning is encoded in the type |
|
||||
|
||||
### Value Object Design Rules
|
||||
|
||||
1. **Immutable.** All fields are set at construction and never change. No setters.
|
||||
2. **Self-validating.** Enforce the type's actual domain constraints, such as a required currency. Whether negative amounts are valid depends on the modeled concept; do not invent a business rule from a generic example.
|
||||
3. **Equality by attributes.** Override `equals()` and `hashCode()` to compare all attributes.
|
||||
4. **Side-effect-free methods.** `money.add(other)` returns a new `Money`; it does not mutate the original.
|
||||
5. **Replace, don't modify.** To change an address, create a new `Address` and assign it. `customer.changeAddress(newAddress)`.
|
||||
|
||||
### Common Value Objects
|
||||
|
||||
| Value Object | Replaces | Why It Is Better |
|
||||
|-------------|---------|------------------|
|
||||
| `Money(amount, currency)` | `BigDecimal` | Prevents currency mismatch errors; encapsulates rounding rules |
|
||||
| `EmailAddress(value)` | `String` | Validates format on construction; impossible to have invalid email in the system |
|
||||
| `DateRange(start, end)` | Two `Date` fields | Enforces `start <= end`; contains overlap/contains logic |
|
||||
| `Address(street, city, state, zip)` | Multiple String fields | Groups related data; validates as a unit |
|
||||
| `Quantity(value, unit)` | `int` or `double` | Prevents unit mismatch (adding kilograms to liters) |
|
||||
| `PhoneNumber(countryCode, number)` | `String` | Validates format; normalizes representation |
|
||||
|
||||
### When to Use Value Objects vs. Entities
|
||||
|
||||
| Signal | Entity | Value Object |
|
||||
|--------|--------|-------------|
|
||||
| Needs to be tracked over time | Yes | No |
|
||||
| Has a lifecycle (created, modified, archived) | Yes | No -- replaced, not modified |
|
||||
| Two instances with same attributes are different things | Yes | No -- they are the same thing |
|
||||
| Immutability is natural | No | Yes |
|
||||
| Appears in the model as a measurement, description, or attribute | No | Yes |
|
||||
|
||||
**Rule of thumb:** If in doubt, make it a Value Object. You can always promote it to an Entity later if identity becomes important. Going the other direction (demoting an Entity to a Value Object) is much harder.
|
||||
|
||||
## Aggregates
|
||||
|
||||
An Aggregate is a cluster of domain objects (entities and value objects) treated as a single unit for data changes. Every aggregate has a single root entity -- the Aggregate Root -- through which all external access occurs.
|
||||
|
||||
### Why Aggregates Exist
|
||||
|
||||
Without aggregates, any object in the system can hold a reference to any other object and modify it directly. This creates an impossibly tangled web of dependencies where enforcing business invariants (rules that must always be true) becomes a nightmare.
|
||||
|
||||
Aggregates solve this by drawing a boundary:
|
||||
- **Inside the boundary:** Strong consistency. All invariants are enforced within a single transaction.
|
||||
- **Across boundaries:** Choose atomic transactions or eventual consistency from the business invariant and accepted storage design.
|
||||
|
||||
### Aggregate Design Rules
|
||||
|
||||
Eric Evans and Vaughn Vernon established these rules, refined by the DDD community:
|
||||
|
||||
#### Rule 1: Protect Business Invariants Inside the Aggregate
|
||||
|
||||
An invariant is a rule that must always be true. Example: "An order's total must equal the sum of its line items." This invariant involves `Order` and `OrderLineItem`. Both belong in the same aggregate because the invariant spans both.
|
||||
|
||||
| If the invariant spans... | Then... |
|
||||
|--------------------------|---------|
|
||||
| A single entity | That entity is its own aggregate |
|
||||
| An entity and its closely related objects | They form one aggregate |
|
||||
| Two independently identifiable things | Determine ownership and atomicity from the invariant; separate aggregates may still share a service transaction |
|
||||
|
||||
#### Rule 2: Small Aggregates
|
||||
|
||||
Large aggregates cause:
|
||||
- **Concurrency conflicts.** Two users editing different parts of the same large aggregate will conflict.
|
||||
- **Performance problems.** Loading a large aggregate means loading everything it contains.
|
||||
- **Transaction scope bloat.** Larger transaction scope means longer locks and more contention.
|
||||
|
||||
**Ideal aggregate size:** One root entity, a small set of value objects, and occasionally a small collection of child entities (e.g., `Order` with `OrderLineItems`).
|
||||
|
||||
**Anti-pattern:** An `Organization` aggregate that contains `Departments` which contain `Employees` which contain `Assignments`. This is too large. `Employee` should be its own aggregate, referencing `Organization` and `Department` by ID.
|
||||
|
||||
#### Rule 3: Reference Other Aggregates by ID Only
|
||||
|
||||
Do not hold direct object references to other aggregates. Instead, store only the identifier:
|
||||
|
||||
**Wrong:**
|
||||
```
|
||||
class Order {
|
||||
Customer customer; // Direct reference to another aggregate
|
||||
}
|
||||
```
|
||||
|
||||
**Right:**
|
||||
```
|
||||
class Order {
|
||||
CustomerId customerId; // Reference by ID only
|
||||
}
|
||||
```
|
||||
|
||||
**Why:** Direct references create tight coupling, prevent independent scaling, and make it impossible to enforce aggregate boundaries. With ID references, each aggregate can be loaded, stored, and cached independently.
|
||||
|
||||
#### Rule 4: Preserve Required Consistency Across Aggregate Boundaries
|
||||
|
||||
Ask whether a partial outcome is valid. When a business operation must commit or roll back together, use a supported service-layer transaction. This repository explicitly uses Mongo transactions across aggregate documents for rental operations; follow [ADR-0011](../../../../docs/adr/0011-rental-aggregate-boundaries.md).
|
||||
|
||||
When delay and partial progress are acceptable, an event-driven alternative is:
|
||||
|
||||
1. The first aggregate performs its action and publishes a domain event
|
||||
2. An event handler picks up the event and modifies the second aggregate in a separate transaction
|
||||
|
||||
**Example:**
|
||||
- `Order.place()` publishes `OrderPlaced` event
|
||||
- `InventoryHandler` receives `OrderPlaced` and calls `inventory.reserve(items)`
|
||||
- These are two separate transactions
|
||||
|
||||
### Choosing Aggregate Boundaries
|
||||
|
||||
#### Start with the Invariant
|
||||
|
||||
Identify every business invariant. Group objects that participate in the same invariant into the same aggregate.
|
||||
|
||||
**Example invariants:**
|
||||
| Invariant | Objects Involved | Aggregate |
|
||||
|-----------|-----------------|-----------|
|
||||
| "An order total must equal the sum of its lines" | Order, OrderLineItem | Order aggregate |
|
||||
| "A product must have at least one category" | Product, Category | Product aggregate (Category is a value object or ID reference) |
|
||||
| "An account balance must never go below the overdraft limit" | Account | Account aggregate (single entity) |
|
||||
| "A reservation cannot overlap with another for the same room" | Reservation, occupancy | Enforce concurrent reservations through shared occupancy/version constraints or a suitable database constraint; a query alone may race |
|
||||
|
||||
#### The Transaction Boundary Test
|
||||
|
||||
Ask: "Must these two changes happen atomically, or can they happen with a small delay?"
|
||||
|
||||
- If **atomically**: consider cohesive aggregate ownership or a supported cross-aggregate transaction, according to domain boundaries and accepted decisions
|
||||
- If **small delay is acceptable**: separate aggregates with eventual consistency
|
||||
|
||||
Establish acceptable delay with domain requirements; do not assume eventual consistency is safe outside financial operations.
|
||||
|
||||
### Aggregate Root Pattern
|
||||
|
||||
The Aggregate Root is the single entity through which all external interaction with the aggregate occurs:
|
||||
|
||||
**Rules for the root:**
|
||||
1. External objects may only hold references to the root, never to internal entities
|
||||
2. All changes to the aggregate go through the root's methods
|
||||
3. The root enforces all aggregate invariants
|
||||
4. The root controls the lifecycle of all internal objects
|
||||
5. Delete the root and everything inside the aggregate is deleted
|
||||
|
||||
**Example:**
|
||||
```
|
||||
// External code interacts only with Order (the root)
|
||||
order.addLineItem(product, quantity, price)
|
||||
order.removeLineItem(lineItemId)
|
||||
order.calculateTotal()
|
||||
|
||||
// LineItems are never accessed directly from outside
|
||||
// WRONG: lineItem.changeQuantity(5)
|
||||
// RIGHT: order.changeLineItemQuantity(lineItemId, 5)
|
||||
```
|
||||
|
||||
### Common Aggregate Mistakes
|
||||
|
||||
| Mistake | Consequence | Fix |
|
||||
|---------|------------|-----|
|
||||
| Making the entire object graph one aggregate | Concurrency nightmares, slow loading | Split into multiple aggregates; reference by ID |
|
||||
| Holding direct references to other aggregates | Tight coupling; cannot enforce boundaries | Replace with ID references |
|
||||
| Choosing coordination without the business invariant | Partial outcomes or unnecessary contention | Preserve required atomicity; choose supported transactions or eventual consistency with explicit failure handling |
|
||||
| Putting all logic in services instead of the aggregate root | Anemic aggregate; invariants not enforced | Move invariant-enforcing logic into the aggregate root |
|
||||
| Creating aggregates based on database tables | Data model drives domain model (backward) | Design aggregates from domain invariants, then map to persistence |
|
||||
|
||||
## Putting It All Together
|
||||
|
||||
A well-designed domain model has this structure:
|
||||
|
||||
1. **Value Objects** form the majority of types -- measurements, descriptions, identifiers, small composites
|
||||
2. **Entities** represent things with identity and lifecycle -- fewer than you think
|
||||
3. **Aggregates** cluster related entities and value objects behind a root -- enforcing consistency boundaries
|
||||
4. **References between aggregates** are by ID only -- enabling independent evolution
|
||||
5. **Cross-aggregate consistency** follows the invariant and accepted storage guarantees; events are an option, not a default requirement
|
||||
|
||||
The result is a model that is expressive (reads like the business), consistent (invariants are enforced), and scalable (aggregates are independent units of consistency, persistence, and caching).
|
||||
Exercise the actual operation, including invalid transitions, stale versions, duplicate commands, and competing callers where relevant. Prove the invariant at the storage/integration boundary when an in-memory test cannot establish it. Preserve distinct existing test coverage when moving behavior behind a new interface.
|
||||
@@ -1,264 +1,29 @@
|
||||
# Domain Events
|
||||
|
||||
A domain event represents a fact the domain cares about. Use events when notification or asynchronous coordination is actually needed, not merely because an operation crosses an aggregate boundary. Preserve the atomic transactions required by the project's accepted ADRs.
|
||||
Use events for a real notification or asynchronous coordination need. Crossing an aggregate boundary alone is not a reason to introduce an event bus.
|
||||
|
||||
## What Domain Events Are
|
||||
## Define the fact and consumers
|
||||
|
||||
A domain event captures a meaningful occurrence in the business domain. "Meaningful" means that a domain expert would recognize it as significant -- not just a technical state change.
|
||||
Name a domain event as a completed business fact, distinct from a command or pending operation. Identify its actual consumers and the minimum non-sensitive data they need. Avoid serializing the entire aggregate or inventing metadata fields without a purpose.
|
||||
|
||||
### Domain Events vs. Technical Events
|
||||
Distinguish internal events from externally consumed contracts. Translate when semantics differ; do not introduce a separate layer merely to change the name.
|
||||
|
||||
| Domain Event | Technical Event | Why It Matters |
|
||||
|-------------|----------------|----------------|
|
||||
| `OrderPlaced` | `RowInserted` | The domain expert cares about orders being placed; they do not care about database rows |
|
||||
| `PaymentReceived` | `WebhookProcessed` | The business reacts to payments; the webhook is an implementation detail |
|
||||
| `ClaimDenied` | `StatusUpdated` | Denial triggers business processes (appeals, notifications); a status update triggers nothing meaningful |
|
||||
| `InventoryDepleted` | `CountReachedZero` | The business has specific procedures for depleted inventory; zero is just a number |
|
||||
## Persistence and delivery
|
||||
|
||||
### The Litmus Test
|
||||
An event must not claim a state change that rolled back. If delivery after commit must be durable, use the project's existing transactional publication mechanism or justify an outbox that records the state change and pending event atomically.
|
||||
|
||||
Ask a domain expert: "Would you care if this happened?" If yes, it is a domain event. If they shrug, it is a technical event that belongs in infrastructure, not in the domain model.
|
||||
For retried delivery, make handlers idempotent using an appropriate key, conditional write, or naturally idempotent operation. Coordinate deduplication and the effect atomically where required; recording a key before a failed effect can lose work.
|
||||
|
||||
## Naming Domain Events
|
||||
Define ordering requirements, retry limits, and observable failure handling. Test duplicate, delayed, out-of-order, and failing delivery where those modes affect the invariant. Do not claim exactly-once behavior merely because duplicate messages are filtered.
|
||||
|
||||
### The Past-Tense Rule
|
||||
## Do not weaken atomic business operations
|
||||
|
||||
Domain events are always named in past tense because they represent facts that have already occurred. By the time anyone processes the event, the thing has already happened.
|
||||
Preserve the cross-aggregate transactions accepted in [ADR-0011](../../../../docs/adr/0011-rental-aggregate-boundaries.md). Asynchronous coordination is suitable only when partial progress and delay are valid. A saga or process manager is justified only by an actual long-running workflow, with explicit recovery semantics.
|
||||
|
||||
**Correct naming:**
|
||||
- `OrderPlaced` -- an order was placed
|
||||
- `PaymentReceived` -- a payment was received
|
||||
- `ShipmentDispatched` -- a shipment was dispatched
|
||||
- `AccountSuspended` -- an account was suspended
|
||||
- `PolicyRenewed` -- a policy was renewed
|
||||
## Event sourcing is a separate choice
|
||||
|
||||
**Incorrect naming:**
|
||||
- `PlaceOrder` -- this is a command, not an event
|
||||
- `OrderPlacing` -- this implies the action is in progress
|
||||
- `OrderEvent` -- too generic; what happened?
|
||||
- `OrderUpdate` -- "update" is not a domain concept; what specifically changed?
|
||||
Event sourcing makes stored events the source of truth; publishing an event does not require it. Consider it only for a demonstrated need for historical reconstruction or temporal queries that the existing model cannot reasonably serve.
|
||||
|
||||
### Naming Specificity
|
||||
If chosen, define replay behavior without repeating external side effects, concurrency protection, and the actual retention obligations. Snapshots are an optimization, not initial scaffolding.
|
||||
|
||||
Be specific about what happened. Vague event names create the same problems as vague method names -- consumers cannot understand what occurred without reading the payload.
|
||||
|
||||
| Vague | Specific | Why Specific Is Better |
|
||||
|-------|----------|----------------------|
|
||||
| `OrderChanged` | `OrderItemAdded`, `OrderItemRemoved`, `OrderAddressChanged` | Different changes trigger different business reactions |
|
||||
| `UserUpdated` | `UserEmailVerified`, `UserPasswordChanged`, `UserProfileCompleted` | A password change requires a security audit; a profile completion triggers onboarding flow |
|
||||
| `PaymentProcessed` | `PaymentAuthorized`, `PaymentCaptured`, `PaymentRefunded` | Authorization and capture are distinct business steps with different downstream effects |
|
||||
|
||||
### Event Naming Conventions
|
||||
|
||||
Adopt a consistent naming pattern across the codebase:
|
||||
|
||||
```
|
||||
{AggregateType}{DomainAction}
|
||||
```
|
||||
|
||||
Examples:
|
||||
- `OrderPlaced`, `OrderCancelled`, `OrderFulfilled`
|
||||
- `InvoiceSent`, `InvoicePaid`, `InvoiceOverdue`
|
||||
- `MemberRegistered`, `MemberSuspended`, `MemberReinstated`
|
||||
|
||||
## Event Structure
|
||||
|
||||
A well-designed domain event contains:
|
||||
|
||||
| Field | Purpose | Example |
|
||||
|-------|---------|---------|
|
||||
| `eventId` | Unique identifier for this specific event occurrence | `uuid("a1b2c3d4...")` |
|
||||
| `eventType` | The name of the event | `"OrderPlaced"` |
|
||||
| `occurredAt` | When the event happened | `"2024-03-15T14:30:00Z"` |
|
||||
| `aggregateId` | The ID of the aggregate that produced the event | `orderId: "ORD-12345"` |
|
||||
| `aggregateType` | The type of aggregate | `"Order"` |
|
||||
| `payload` | The domain-relevant data | `{ customerId, items, total, shippingAddress }` |
|
||||
| `metadata` | Technical metadata (correlation ID, causation ID, user ID) | `{ correlationId, userId }` |
|
||||
|
||||
### What Goes in the Payload
|
||||
|
||||
Include enough data for consumers to react without calling back to the producer:
|
||||
|
||||
**Too little:**
|
||||
```json
|
||||
{ "orderId": "ORD-12345" }
|
||||
```
|
||||
Every consumer must call back to the Order service to get details. This creates coupling and latency.
|
||||
|
||||
**Too much:**
|
||||
```json
|
||||
{ "order": { /* entire order aggregate serialized */ } }
|
||||
```
|
||||
This bloats messages, exposes internal model details, and creates tight coupling to the aggregate structure.
|
||||
|
||||
**Just right:**
|
||||
```json
|
||||
{
|
||||
"orderId": "ORD-12345",
|
||||
"customerId": "CUST-789",
|
||||
"items": [
|
||||
{ "productId": "PROD-1", "quantity": 2, "unitPrice": 29.99 }
|
||||
],
|
||||
"totalAmount": 59.98,
|
||||
"currency": "USD",
|
||||
"shippingAddress": { "city": "Springfield", "state": "IL" }
|
||||
}
|
||||
```
|
||||
Enough for most consumers to react; detailed enough to avoid callbacks for common cases.
|
||||
|
||||
## Publishing Domain Events
|
||||
|
||||
### Where Events Are Raised
|
||||
|
||||
Domain events are raised within the aggregate, as part of the domain operation that caused them:
|
||||
|
||||
```
|
||||
class Order:
|
||||
def place(self):
|
||||
self._validate_can_be_placed()
|
||||
self.status = OrderStatus.PLACED
|
||||
self._raise_event(OrderPlaced(
|
||||
order_id=self.id,
|
||||
customer_id=self.customer_id,
|
||||
items=self.items,
|
||||
total=self.total
|
||||
))
|
||||
```
|
||||
|
||||
The aggregate records the event internally. An infrastructure mechanism (event dispatcher, outbox pattern) publishes it after the aggregate is persisted.
|
||||
|
||||
### The Outbox Pattern
|
||||
|
||||
The most reliable way to publish domain events is the transactional outbox:
|
||||
|
||||
1. Within the same database transaction that persists the aggregate, insert the event into an `outbox` table
|
||||
2. A separate process (poller or CDC -- Change Data Capture) reads from the outbox and publishes to the message broker
|
||||
3. After successful publication, mark the outbox entry as published
|
||||
|
||||
**Why this matters:** If you publish the event and then save the aggregate, the save might fail -- you published a lie. If you save the aggregate and then publish, the publish might fail -- the event is lost. The outbox pattern ties both operations to the same database transaction.
|
||||
|
||||
### Delivery Guarantees
|
||||
|
||||
| Guarantee | Meaning | Implementation |
|
||||
|-----------|---------|----------------|
|
||||
| At-most-once | Events may be lost but never duplicated | Fire-and-forget; no outbox; acceptable for non-critical events |
|
||||
| At-least-once | Events are never lost but may be duplicated | Outbox pattern with retry; consumers must be idempotent |
|
||||
| Exactly-once | Events are delivered exactly once | Practically impossible in distributed systems; achieve via at-least-once + idempotent consumers |
|
||||
|
||||
**At-least-once with idempotent consumers** is the standard approach. Design every event handler to be safe to run multiple times with the same event.
|
||||
|
||||
## Domain Events for Cross-Context Integration
|
||||
|
||||
### Internal vs. Integration Events
|
||||
|
||||
| Aspect | Domain Event (Internal) | Integration Event (External) |
|
||||
|--------|------------------------|------------------------------|
|
||||
| Scope | Within a bounded context | Across bounded contexts |
|
||||
| Audience | Event handlers in the same context | Other teams' services |
|
||||
| Schema | Change with the model and its consumers | Honor actual external contracts; this application's development consumers change together, without compatibility versions |
|
||||
| Naming | Uses internal ubiquitous language | Uses published language (shared schema) |
|
||||
| Transport | In-process event bus or same database | Message broker (Kafka, RabbitMQ, SNS) |
|
||||
|
||||
### Translation at the Boundary
|
||||
|
||||
When a domain event crosses a bounded context boundary, it should be translated into an integration event that uses the published language:
|
||||
|
||||
1. **Order context** raises `OrderPlaced` (domain event, internal language)
|
||||
2. **Anti-corruption layer** translates to `PurchaseCompleted` (integration event, published language)
|
||||
3. **Shipping context** receives `PurchaseCompleted` and translates to `ShipmentRequested` (domain event, shipping language)
|
||||
|
||||
This translation prevents internal model changes from breaking external consumers.
|
||||
|
||||
### Event-Driven Architecture Patterns
|
||||
|
||||
#### Event Notification
|
||||
|
||||
The event carries minimal data ("something happened") and consumers call back for details. Simplest pattern but creates temporal coupling.
|
||||
|
||||
#### Event-Carried State Transfer
|
||||
|
||||
The event carries all the data consumers need. Consumers maintain their own local copy of relevant data, reducing coupling but increasing event size and requiring consumers to maintain projections.
|
||||
|
||||
#### Event Sourcing
|
||||
|
||||
Events are the source of truth. Current state is derived by replaying events. This is the most powerful and most complex pattern.
|
||||
|
||||
## Event Sourcing
|
||||
|
||||
Event sourcing stores the complete history of state changes as an ordered sequence of events. Instead of storing only the current state ("account balance is $1,000"), the system stores every event that led to that state ("deposited $500, deposited $800, withdrew $300").
|
||||
|
||||
### When to Use Event Sourcing
|
||||
|
||||
| Good Fit | Poor Fit |
|
||||
|----------|----------|
|
||||
| Audit requirements (financial, medical, legal) | Simple CRUD with no audit needs |
|
||||
| Complex domain with many state transitions | Domains with few state changes |
|
||||
| Need to answer "how did we get here?" | Only need current state |
|
||||
| Need to rebuild state at any point in time | No temporal query requirements |
|
||||
| High-value domain events that are worth preserving | High-volume, low-value data (telemetry) |
|
||||
|
||||
### Event Sourcing Mechanics
|
||||
|
||||
**Storing events:**
|
||||
```
|
||||
Stream: Order-12345
|
||||
1: OrderCreated { customerId, items }
|
||||
2: PaymentAuthorized { paymentId, amount }
|
||||
3: OrderConfirmed { confirmedAt }
|
||||
4: ItemShipped { trackingNumber, items }
|
||||
5: OrderDelivered { deliveredAt, signedBy }
|
||||
```
|
||||
|
||||
**Rebuilding state:**
|
||||
```
|
||||
currentState = OrderCreated.apply(emptyOrder)
|
||||
currentState = PaymentAuthorized.apply(currentState)
|
||||
currentState = OrderConfirmed.apply(currentState)
|
||||
currentState = ItemShipped.apply(currentState)
|
||||
currentState = OrderDelivered.apply(currentState)
|
||||
```
|
||||
|
||||
**Snapshots** optimize performance: periodically save the current state so you do not need to replay from the beginning every time.
|
||||
|
||||
### Event Sourcing Challenges
|
||||
|
||||
| Challenge | Solution |
|
||||
|-----------|----------|
|
||||
| Event schema evolution | Replace and reseed disposable development events. Only consider retained-event upcasters after an explicit decision requires preserving production history |
|
||||
| Performance with long event streams | Snapshots at regular intervals; read models for queries |
|
||||
| Complexity | Only use event sourcing for aggregates where it provides clear value; not everything needs to be event-sourced |
|
||||
| Debugging | Event logs provide excellent debugging and auditing; invest in tooling to browse and replay events |
|
||||
|
||||
## Patterns for Event Handling
|
||||
|
||||
### Idempotent Handlers
|
||||
|
||||
Every event handler must be safe to execute multiple times with the same event. Strategies:
|
||||
|
||||
- **Idempotency key:** Store processed event IDs; skip duplicates
|
||||
- **Idempotent operations:** Design the operation itself to be naturally idempotent (e.g., "set balance to X" instead of "add Y to balance")
|
||||
- **Conditional writes:** Use optimistic concurrency (version checks) to prevent double-application
|
||||
|
||||
### Ordering Guarantees
|
||||
|
||||
Events from the same aggregate should be processed in order. Events from different aggregates have no ordering guarantees. Design consumers accordingly:
|
||||
|
||||
- Partition by aggregate ID in the message broker (Kafka partition key = aggregate ID)
|
||||
- Handle out-of-order events gracefully (check event version, buffer and reorder if needed)
|
||||
|
||||
### Dead Letter Handling
|
||||
|
||||
Events that repeatedly fail to process should be routed to a dead letter queue for investigation. Never silently drop failed events. Monitor dead letter queues actively.
|
||||
|
||||
### Sagas and Process Managers
|
||||
|
||||
Long-running business processes that span multiple aggregates or bounded contexts can be coordinated using sagas:
|
||||
|
||||
1. `OrderPlaced` triggers the saga
|
||||
2. Saga sends `ReserveInventory` command
|
||||
3. `InventoryReserved` event continues the saga
|
||||
4. Saga sends `AuthorizePayment` command
|
||||
5. `PaymentAuthorized` event continues the saga
|
||||
6. If any step fails, the saga sends compensating commands (`ReleaseInventory`, `RefundPayment`)
|
||||
|
||||
Sagas maintain their own state and react to events. They do not hold locks -- they coordinate through events and compensating actions.
|
||||
Follow [the development lifecycle](../../../../AGENTS.md#development-data-lifecycle): replace and reseed disposable development events. Retained-history upcasters or compatibility versions require an explicit later production-data decision; genuine external event contracts remain external constraints.
|
||||
@@ -1,353 +1,25 @@
|
||||
# Repositories and Factories
|
||||
|
||||
Repositories and Factories are infrastructure-facing patterns in Domain-Driven Design that separate domain logic from persistence and object creation concerns. The Repository provides the illusion of an in-memory collection of aggregates. The Factory encapsulates complex creation logic. Together, they keep the domain model clean and focused on business rules.
|
||||
Use this branch when the task concerns persistence contracts or complex object creation, not as a checklist of layers to add.
|
||||
|
||||
## Repository contracts
|
||||
|
||||
## Table of Contents
|
||||
1. [The Repository Pattern](#the-repository-pattern)
|
||||
2. [The Factory Pattern](#the-factory-pattern)
|
||||
3. [The Specification Pattern](#the-specification-pattern)
|
||||
4. [Ports and Adapters Relationship](#ports-and-adapters-relationship)
|
||||
Keep domain decisions separate from storage mechanics. Define methods in terms of actual domain operations and queries, following the repository's existing architecture. Avoid generic query escape hatches that leak SQL or document details into domain behavior.
|
||||
|
||||
---
|
||||
Aggregate-loading methods return enough valid state for the operation to enforce its invariants. Query-only methods may return explicit read projections; this does not require a new CQRS subsystem. Fetch only needed data and test relevant storage, transaction, and concurrency behavior.
|
||||
|
||||
## The Repository Pattern
|
||||
Follow the persistence technology's actual save/change-tracking semantics. Make absence, failed writes, and stale versions explicit in the contract.
|
||||
|
||||
A Repository mediates between the domain and data mapping layers, acting like an in-memory collection of domain objects. Domain code uses the repository to obtain aggregates without knowing how they are stored, queried, or reconstructed.
|
||||
## Reconstruction versus creation
|
||||
|
||||
### Why Repositories Exist
|
||||
Creating a new aggregate validates command inputs and invariants and may record creation events. Loading an existing aggregate preserves its identity and must not replay creation events or other external effects.
|
||||
|
||||
Without repositories, domain logic becomes tangled with data access:
|
||||
Validate the persisted shape and invariants needed for safe use rather than assuming stored data is trustworthy. Report corrupt or incompatible records; apply the project's disposable-data policy instead of adding legacy readers.
|
||||
|
||||
```
|
||||
// Without repository -- domain logic polluted with SQL
|
||||
def approve_claim(claim_id):
|
||||
row = db.execute("SELECT * FROM claims WHERE id = ?", claim_id)
|
||||
claim = Claim(row['id'], row['status'], row['amount'])
|
||||
claim.approve()
|
||||
db.execute("UPDATE claims SET status = ? WHERE id = ?", claim.status, claim.id)
|
||||
```
|
||||
## Factories and query specifications
|
||||
|
||||
```
|
||||
// With repository -- domain logic is clean
|
||||
def approve_claim(claim_id):
|
||||
claim = claim_repository.find_by_id(claim_id)
|
||||
claim.approve()
|
||||
claim_repository.save(claim)
|
||||
```
|
||||
Use a constructor or existing mapper for straightforward creation. A factory earns its place when assembly or selection logic is sufficiently complex, duplicated, or independently testable; it must not produce an invalid aggregate.
|
||||
|
||||
The second version is readable by a domain expert. The first is not.
|
||||
Use query specifications only when actual reusable composition warrants them. A focused repository method is often enough.
|
||||
|
||||
### Repository Interface Design
|
||||
|
||||
The repository interface belongs in the domain layer. It speaks the ubiquitous language:
|
||||
|
||||
**Good repository methods:**
|
||||
- `find_by_id(order_id)` -- straightforward identity lookup
|
||||
- `find_pending_orders()` -- uses domain language ("pending")
|
||||
- `find_by_customer(customer_id)` -- domain-meaningful query
|
||||
- `find_overdue_invoices(as_of_date)` -- business concept in the method name
|
||||
|
||||
**Bad repository methods:**
|
||||
- `get_by_status_code(3)` -- magic number; what is status 3?
|
||||
- `query(sql_string)` -- leaks persistence technology into the domain
|
||||
- `find_all_with_joins()` -- technical concern, not domain language
|
||||
- `get_by_column("status", "PENDING")` -- generic data access, not domain query
|
||||
|
||||
### Collection-Oriented vs. Persistence-Oriented Repositories
|
||||
|
||||
Eric Evans described two flavors of repository, each modeling a different metaphor:
|
||||
|
||||
#### Collection-Oriented Repository
|
||||
|
||||
Models the repository as an in-memory collection. You add objects to it and remove objects from it. Changes to retrieved objects are automatically tracked and persisted (like JPA/Hibernate managed entities).
|
||||
|
||||
```
|
||||
interface OrderRepository:
|
||||
add(order) # Like collection.add()
|
||||
remove(order) # Like collection.remove()
|
||||
find_by_id(id) # Like collection.find()
|
||||
# No explicit save() -- changes to retrieved objects are auto-tracked
|
||||
```
|
||||
|
||||
**Best with:** ORMs that support change tracking (JPA/Hibernate, Entity Framework).
|
||||
|
||||
**Advantages:** Clean domain model; changes feel natural; no explicit save calls.
|
||||
|
||||
**Disadvantages:** "Magic" change tracking can surprise developers; harder to reason about when persistence happens.
|
||||
|
||||
#### Persistence-Oriented Repository
|
||||
|
||||
Models the repository as a storage mechanism. You explicitly save objects and the repository does not track changes automatically.
|
||||
|
||||
```
|
||||
interface OrderRepository:
|
||||
save(order) # Explicit persist/update
|
||||
delete(order_id) # Explicit remove
|
||||
find_by_id(id) # Retrieve
|
||||
# Must call save() explicitly after making changes
|
||||
```
|
||||
|
||||
**Best with:** Frameworks without change tracking (most non-ORM approaches, event sourcing, document stores).
|
||||
|
||||
**Advantages:** Explicit control over when persistence happens; no surprises; easier to test.
|
||||
|
||||
**Disadvantages:** Must remember to call save(); risk of losing changes if save is forgotten.
|
||||
|
||||
**Which to choose:** If your persistence technology offers change tracking and your team is comfortable with it, use collection-oriented. Otherwise, use persistence-oriented. The persistence-oriented style is more common in modern applications because it is more explicit.
|
||||
|
||||
### Repository Implementation
|
||||
|
||||
The repository interface lives in the domain layer. The implementation lives in the infrastructure layer. This is the Dependency Inversion Principle in action:
|
||||
|
||||
```
|
||||
domain/
|
||||
model/
|
||||
Order.py # Aggregate root
|
||||
OrderLineItem.py # Entity within aggregate
|
||||
repository/
|
||||
OrderRepository.py # Interface (abstract class / protocol)
|
||||
|
||||
infrastructure/
|
||||
persistence/
|
||||
PostgresOrderRepository.py # Implementation
|
||||
InMemoryOrderRepository.py # Implementation for tests
|
||||
```
|
||||
|
||||
The domain layer defines what it needs (the interface). The infrastructure layer provides it (the implementation). The domain never imports from infrastructure.
|
||||
|
||||
### What a Repository Returns
|
||||
|
||||
An aggregate-loading method returns a complete valid aggregate for the operation, not an ambiguously partial object. Read-only query methods may return explicit projections according to the repository's existing architecture; this does not by itself justify adding a CQRS subsystem.
|
||||
|
||||
**Correct:** `order_repository.find_by_id(id)` returns an `Order` with all its `OrderLineItems` loaded, ready to have business operations performed on it.
|
||||
|
||||
**Incorrect:** `order_repository.find_by_id(id)` returns an `OrderDTO` with some fields populated and others lazily loaded. The caller must check which fields are available.
|
||||
|
||||
### Repository Anti-Patterns
|
||||
|
||||
| Anti-Pattern | Problem | Fix |
|
||||
|-------------|---------|-----|
|
||||
| Generic repository (`Repository<T>`) | All aggregates look the same; domain-specific queries do not fit the generic interface | Create specific repository interfaces per aggregate type |
|
||||
| Partial object used as an aggregate | Behavior runs without the state needed to enforce invariants | Load the required aggregate; use explicit read projections for query-only operations |
|
||||
| Repository per entity (not per aggregate) | Bypasses aggregate root; allows direct modification of internal entities | One repository per aggregate root only |
|
||||
| Repository with business logic | Repository starts containing validation or transformation logic | Keep repositories as pure storage; domain logic belongs in the aggregate |
|
||||
| Repository depends on domain services | Circular dependency between domain services and repositories | Repositories depend only on the domain model (aggregates, value objects) |
|
||||
|
||||
## The Factory Pattern
|
||||
|
||||
A Factory encapsulates the logic of creating a domain object, ensuring that the object is fully formed and valid from the moment it exists. In DDD, factories are used when object creation is complex enough to warrant its own abstraction.
|
||||
|
||||
### When to Use a Factory
|
||||
|
||||
| Situation | Factory Needed? | Why |
|
||||
|-----------|----------------|-----|
|
||||
| Creating a Value Object with 2-3 fields | No | A constructor suffices: `Money(100, "USD")` |
|
||||
| Creating an aggregate with multiple parts and validation rules | Yes | The assembly logic is complex; a constructor would be enormous |
|
||||
| Creating an object from an external representation (API response, file) | When complexity warrants | Validate and translate at the boundary; an existing mapper or constructor may suffice |
|
||||
| Creating an object with conditional logic (different subtypes) | When complexity warrants | Keep the selection cohesive without adding an unnecessary factory abstraction |
|
||||
| Reconstituting an object from persistence | Maybe | If the repository handles it, a separate factory may not be needed |
|
||||
|
||||
### Factory Patterns in DDD
|
||||
|
||||
#### Factory Method on the Aggregate
|
||||
|
||||
The most common pattern: a static or class method on the aggregate itself:
|
||||
|
||||
```
|
||||
class Order:
|
||||
@staticmethod
|
||||
def create_from_cart(cart, customer_id):
|
||||
# Validates cart is not empty
|
||||
# Converts cart items to OrderLineItems
|
||||
# Calculates initial total
|
||||
# Returns a fully valid Order
|
||||
order = Order(
|
||||
id=OrderId.generate(),
|
||||
customer_id=customer_id,
|
||||
status=OrderStatus.PENDING,
|
||||
items=[OrderLineItem.from_cart_item(item) for item in cart.items],
|
||||
total=cart.calculate_total()
|
||||
)
|
||||
order._raise_event(OrderCreated(...))
|
||||
return order
|
||||
```
|
||||
|
||||
**Advantages:** Creation logic lives close to the aggregate. The aggregate controls its own birth.
|
||||
|
||||
#### Factory Method on Another Aggregate
|
||||
|
||||
When one aggregate creates another:
|
||||
|
||||
```
|
||||
class Quote:
|
||||
def convert_to_order(self):
|
||||
# Quote knows how to create an Order from itself
|
||||
order = Order(
|
||||
id=OrderId.generate(),
|
||||
customer_id=self.customer_id,
|
||||
items=[OrderLineItem(item.product_id, item.quantity, item.quoted_price)
|
||||
for item in self.line_items],
|
||||
source_quote_id=self.id
|
||||
)
|
||||
return order
|
||||
```
|
||||
|
||||
#### Standalone Factory
|
||||
|
||||
When creation logic does not naturally belong to any existing aggregate:
|
||||
|
||||
```
|
||||
class LoanApplicationFactory:
|
||||
def create_from_submission(self, submission, credit_report):
|
||||
# Complex assembly involving multiple inputs
|
||||
# Conditional logic based on loan type
|
||||
# Validation against business rules
|
||||
applicant = Applicant(submission.name, submission.ssn)
|
||||
risk_score = RiskScore.calculate(credit_report)
|
||||
|
||||
if submission.loan_type == "mortgage":
|
||||
return MortgageApplication(applicant, risk_score, submission.property)
|
||||
elif submission.loan_type == "auto":
|
||||
return AutoLoanApplication(applicant, risk_score, submission.vehicle)
|
||||
```
|
||||
|
||||
### Factory Invariants
|
||||
|
||||
The most critical rule of factories: **a factory must never produce an invalid object.** If the inputs are insufficient or violate business rules, the factory must fail (throw an exception), not produce a partially valid object.
|
||||
|
||||
```
|
||||
# Good -- factory enforces invariants
|
||||
class Order:
|
||||
@staticmethod
|
||||
def create(customer_id, items):
|
||||
if not items:
|
||||
raise EmptyOrderError("Cannot create an order with no items")
|
||||
if not customer_id:
|
||||
raise InvalidCustomerError("Order requires a customer")
|
||||
return Order(OrderId.generate(), customer_id, items)
|
||||
|
||||
# Bad -- factory produces potentially invalid objects
|
||||
class Order:
|
||||
@staticmethod
|
||||
def create(customer_id=None, items=None):
|
||||
return Order(OrderId.generate(), customer_id, items or [])
|
||||
# caller can now have an order with no customer and no items
|
||||
```
|
||||
|
||||
### Reconstitution vs. Creation
|
||||
|
||||
There is an important distinction between creating a new aggregate and reconstituting one from persistence:
|
||||
|
||||
| Aspect | Creation | Reconstitution |
|
||||
|--------|---------|----------------|
|
||||
| When | A new domain object comes into existence | An existing object is loaded from storage |
|
||||
| Validation | Validate new-command inputs and invariants | Validate persisted shape and invariants required for safe use; do not replay creation-only side effects |
|
||||
| Domain events | May raise creation events (`OrderCreated`) | Should NOT raise events; nothing new happened |
|
||||
| Identity | Generate a new ID | Use the stored ID |
|
||||
| Invariants | Enforce the new object's invariants | Establish required invariants at the persistence boundary; report corrupt/incompatible records rather than trusting them blindly |
|
||||
|
||||
Reconstitution typically happens inside the repository implementation:
|
||||
|
||||
```
|
||||
class PostgresOrderRepository:
|
||||
def find_by_id(self, order_id):
|
||||
row = self.db.query("SELECT * FROM orders WHERE id = ?", order_id)
|
||||
items = self.db.query("SELECT * FROM order_items WHERE order_id = ?", order_id)
|
||||
# Reconstitute with structural/invariant checks, without new creation events
|
||||
return Order._reconstitute(
|
||||
id=row['id'],
|
||||
customer_id=row['customer_id'],
|
||||
status=row['status'],
|
||||
items=[OrderLineItem._reconstitute(i) for i in items]
|
||||
)
|
||||
```
|
||||
|
||||
## The Specification Pattern
|
||||
|
||||
The Specification pattern encapsulates query criteria as first-class domain objects. Instead of building queries in service code, you express criteria as composable specification objects.
|
||||
|
||||
### Why Specifications
|
||||
|
||||
Without specifications, query logic scatters across the codebase:
|
||||
|
||||
```
|
||||
# Query logic in a service -- not reusable, not composable
|
||||
def find_risky_orders(self):
|
||||
return db.query("SELECT * FROM orders WHERE total > 10000 AND customer_risk > 7")
|
||||
|
||||
# Same logic duplicated elsewhere with slight variations
|
||||
def find_very_risky_orders(self):
|
||||
return db.query("SELECT * FROM orders WHERE total > 50000 AND customer_risk > 9")
|
||||
```
|
||||
|
||||
With specifications:
|
||||
|
||||
```
|
||||
high_value = OrderValueExceeds(10000)
|
||||
high_risk = CustomerRiskAbove(7)
|
||||
risky_orders = order_repository.find_matching(high_value.and_(high_risk))
|
||||
|
||||
very_risky = OrderValueExceeds(50000).and_(CustomerRiskAbove(9))
|
||||
very_risky_orders = order_repository.find_matching(very_risky)
|
||||
```
|
||||
|
||||
### Specification Composition
|
||||
|
||||
Specifications compose using logical operators:
|
||||
|
||||
| Operator | Meaning | Example |
|
||||
|----------|---------|---------|
|
||||
| `and_` | Both must be true | `HighValue.and_(HighRisk)` |
|
||||
| `or_` | Either must be true | `HighValue.or_(HighRisk)` |
|
||||
| `not_` | Must not be true | `not_(Cancelled)` |
|
||||
|
||||
### Specifications in the Domain Layer
|
||||
|
||||
The specification interface lives in the domain layer. Implementations can be in the domain (for in-memory filtering) or infrastructure (for database queries):
|
||||
|
||||
```
|
||||
# Domain layer -- specification interface
|
||||
class Specification:
|
||||
def is_satisfied_by(self, candidate) -> bool:
|
||||
pass
|
||||
|
||||
class OverdueInvoice(Specification):
|
||||
def __init__(self, as_of_date):
|
||||
self.as_of_date = as_of_date
|
||||
|
||||
def is_satisfied_by(self, invoice):
|
||||
return invoice.due_date < self.as_of_date and not invoice.is_paid
|
||||
```
|
||||
|
||||
## Ports and Adapters Relationship
|
||||
|
||||
Repositories and Factories fit naturally into the Ports and Adapters (Hexagonal) architecture:
|
||||
|
||||
```
|
||||
Domain Layer
|
||||
┌─────────────────────────┐
|
||||
│ Aggregates │
|
||||
│ Value Objects │
|
||||
│ Domain Events │
|
||||
│ Repository Interfaces ──┼── Port (interface)
|
||||
│ Factory Interfaces ──┼── Port (interface)
|
||||
└─────────────────────────┘
|
||||
│
|
||||
│ implements
|
||||
▼
|
||||
Infrastructure Layer
|
||||
┌─────────────────────────┐
|
||||
│ PostgresOrderRepo ──┼── Adapter (implementation)
|
||||
│ InMemoryOrderRepo ──┼── Adapter (for tests)
|
||||
│ S3DocumentFactory ──┼── Adapter (implementation)
|
||||
└─────────────────────────┘
|
||||
```
|
||||
|
||||
**The key principle:** The domain defines what it needs (ports). Infrastructure provides it (adapters). Dependencies point inward -- infrastructure depends on domain, never the reverse.
|
||||
|
||||
This means:
|
||||
- The domain layer has zero imports from infrastructure packages
|
||||
- Repository interfaces use domain types (`Order`, `OrderId`), not infrastructure types (`Row`, `Document`)
|
||||
- The application can swap persistence technologies by providing a new adapter without touching domain code
|
||||
- Tests use in-memory adapters to test domain logic without databases
|
||||
Keep dependencies aligned with existing domain/application/infrastructure ownership. An in-memory test adapter can isolate domain logic, but does not replace integration checks for database behavior.
|
||||
@@ -1,251 +1,23 @@
|
||||
# Strategic Design and Distillation
|
||||
# Strategic Design
|
||||
|
||||
Strategic design is the practice of identifying which parts of a system matter most and allocating design effort accordingly. Not all code is created equal. Some code is the reason the business exists; other code is necessary plumbing. Domain distillation is the process of separating the essential from the incidental, so that the core of the domain model receives the deepest thought and the best talent.
|
||||
Use this branch when the user asks where to invest modeling effort or whether to build, reuse, or buy a capability.
|
||||
|
||||
## The Three Types of Subdomains
|
||||
## Classify by the actual business
|
||||
|
||||
Eric Evans classifies every part of a system into one of three subdomain types. This classification drives every major design and investment decision.
|
||||
- **Core:** the behavior that provides the product's distinctive value.
|
||||
- **Supporting:** business-specific functionality needed to deliver that value.
|
||||
- **Generic:** a broadly shared problem where existing solutions may fit.
|
||||
|
||||
### Core Domain
|
||||
These are decision aids, not fixed quality tiers or staffing rules. Security, accessibility, and data integrity remain required in every category. A domain's classification can change as the product changes.
|
||||
|
||||
The Core Domain is the part of the system that provides competitive advantage. It is the reason the business exists and what differentiates it from competitors. Without it, the business has no unique value proposition.
|
||||
## Compare concrete options
|
||||
|
||||
**Characteristics:**
|
||||
- Contains the most complex and nuanced business rules
|
||||
- Is the source of competitive advantage
|
||||
- Changes frequently as the business evolves its strategy
|
||||
- Cannot be outsourced without losing differentiation
|
||||
- Requires the deepest domain expertise
|
||||
Start with the existing implementation and stack. Compare options against needed behavior, integration cost, operational burden, lock-in, privacy/security constraints, and available expertise.
|
||||
|
||||
**Examples:**
|
||||
Do not prescribe a vendor, dependency, outsourcing arrangement, or percentage allocation from a generic example. Verify current primary sources before making a real product recommendation and preserve the user's selected product unless changing it is in scope.
|
||||
|
||||
| Business | Core Domain | Why It Is Core |
|
||||
|----------|-------------|----------------|
|
||||
| Amazon | Recommendation engine, marketplace matching, logistics optimization | These are the algorithms that make Amazon uniquely effective |
|
||||
| Stripe | Payment processing, fraud detection, developer experience | These are what make Stripe better than alternatives |
|
||||
| Netflix | Content recommendation, streaming optimization | These keep subscribers engaged and differentiators from competitors |
|
||||
| Insurance company | Risk assessment, claims adjudication, actuarial modeling | These determine profitability and pricing accuracy |
|
||||
| Trading firm | Signal generation, execution algorithms, risk management | These are the source of alpha |
|
||||
## Distill the decision
|
||||
|
||||
**Investment rule:** Put your best developers here. Apply the deepest modeling techniques. This is where DDD patterns earn their complexity cost.
|
||||
Identify the concepts and invariants that deserve the most attention, the surrounding functionality that can stay simple, and the evidence supporting that choice. Update existing domain documents only when requested; propose an ADR for a consequential, non-obvious trade-off rather than creating a parallel architecture handbook.
|
||||
|
||||
### Supporting Subdomain
|
||||
|
||||
A Supporting Subdomain is necessary for the business to function but does not provide competitive advantage. It supports the Core Domain. You build it because off-the-shelf solutions do not fit your specific needs, but you do not need to over-engineer it.
|
||||
|
||||
**Characteristics:**
|
||||
- Custom-built because available solutions do not quite fit
|
||||
- Important but not differentiating
|
||||
- Moderately complex; business-specific but not competitively critical
|
||||
- Can be built by competent developers without deep domain modeling
|
||||
|
||||
**Examples:**
|
||||
|
||||
| Business | Supporting Subdomain | Why It Is Supporting |
|
||||
|----------|---------------------|---------------------|
|
||||
| E-commerce platform | Order management, inventory tracking | Necessary for operations but not what makes this e-commerce site unique |
|
||||
| Insurance company | Policy administration, document generation | Must work correctly but is not a competitive differentiator |
|
||||
| Trading firm | Position reporting, compliance reporting | Regulatory requirement, not a source of trading advantage |
|
||||
| SaaS product | Tenant management, billing integration | Needed but not what customers buy the product for |
|
||||
|
||||
**Investment rule:** Build it, but keep it simple. Use straightforward designs. Do not apply deep DDD modeling patterns unless the complexity warrants it.
|
||||
|
||||
### Generic Subdomain
|
||||
|
||||
A Generic Subdomain is functionality that is common across many businesses and has no business specificity. It is commodity software that you should buy, use open-source, or outsource.
|
||||
|
||||
**Characteristics:**
|
||||
- Not specific to your business; every company needs it
|
||||
- Well-solved problems with mature solutions available
|
||||
- Building it yourself is a waste of your best developers' time
|
||||
- Off-the-shelf solutions are often better than what you would build
|
||||
|
||||
**Examples:**
|
||||
|
||||
| Generic Subdomain | Buy/Use Instead |
|
||||
|-------------------|----------------|
|
||||
| Authentication and authorization | Auth0, Okta, Keycloak, Clerk |
|
||||
| Email sending | SendGrid, Amazon SES, Postmark |
|
||||
| Payment processing (if not your core) | Stripe, Braintree, Adyen |
|
||||
| File storage | Amazon S3, Google Cloud Storage |
|
||||
| Search indexing | Elasticsearch, Algolia, Typesense |
|
||||
| Monitoring and alerting | Datadog, Grafana, PagerDuty |
|
||||
| CMS / Content management | WordPress, Contentful, Sanity |
|
||||
|
||||
**Investment rule:** Do not build this. Buy it, use open-source, or outsource it. Every hour your best developer spends building a custom email sender is an hour stolen from the Core Domain.
|
||||
|
||||
## Identifying Your Core Domain
|
||||
|
||||
### The Differentiation Test
|
||||
|
||||
For each part of the system, ask: "If a competitor had exactly the same implementation of this, would we lose our competitive advantage?"
|
||||
|
||||
- **Yes, we would lose advantage:** Core Domain
|
||||
- **No, but we would be inconvenienced:** Supporting Subdomain
|
||||
- **No, and we could swap it easily:** Generic Subdomain
|
||||
|
||||
### The Outsourcing Test
|
||||
|
||||
Ask: "Could we outsource this to a competent contractor or replace it with a SaaS product without damaging our competitive position?"
|
||||
|
||||
- **No, absolutely not -- this is our secret sauce:** Core Domain
|
||||
- **Maybe, but it would need customization:** Supporting Subdomain
|
||||
- **Yes, easily:** Generic Subdomain
|
||||
|
||||
### The Talent Test
|
||||
|
||||
Ask: "Does working on this require deep expertise in our specific business domain?"
|
||||
|
||||
- **Yes -- only people who deeply understand our industry can get this right:** Core Domain
|
||||
- **Somewhat -- general software skills with some domain knowledge:** Supporting Subdomain
|
||||
- **No -- any competent developer could implement this:** Generic Subdomain
|
||||
|
||||
### Common Misclassification Errors
|
||||
|
||||
| Error | Reality | Consequence |
|
||||
|-------|---------|-------------|
|
||||
| "Everything is core" | Most things are supporting or generic | Best talent spread thin; nothing gets deep modeling |
|
||||
| "Our custom CRM is core" | CRM is generic; your customer relationships are core | Team spent years building what Salesforce does better |
|
||||
| "Authentication is core" | Authentication is generic (unless you are Auth0) | Security expertise wasted on commodity functionality |
|
||||
| "Infrastructure is core" | Infrastructure is generic (unless you are AWS) | Platform team grows while product team starves |
|
||||
| "Our billing system is core" | Billing is usually supporting or generic | Over-engineered billing while the actual product suffered |
|
||||
|
||||
## Domain Distillation
|
||||
|
||||
Distillation is the process of extracting and clarifying the Core Domain from the rest of the system. It makes the most important parts of the model explicit, visible, and well-understood.
|
||||
|
||||
### The Domain Vision Statement
|
||||
|
||||
A Domain Vision Statement is a short document (one page or less) that describes the Core Domain's value proposition and its most important aspects. It serves as a north star for the team.
|
||||
|
||||
**What it contains:**
|
||||
- What makes this domain unique and valuable
|
||||
- What distinguishes the Core Domain from everything else in the system
|
||||
- What the domain model must capture to deliver competitive advantage
|
||||
- What the team should focus on and what they should explicitly not focus on
|
||||
|
||||
**Example for an insurance company:**
|
||||
|
||||
> Our competitive advantage is our ability to accurately assess risk in real-time for commercial property insurance. Our core domain model must capture the nuanced relationships between property characteristics, geographic risk factors, historical claims data, and market conditions. The model must support rapid repricing as conditions change. Everything else -- policy administration, document generation, payment processing -- is supporting infrastructure that must work correctly but does not differentiate us.
|
||||
|
||||
### The Highlighted Core
|
||||
|
||||
The Highlighted Core is a technique for making the Core Domain visually obvious in the codebase and in documentation.
|
||||
|
||||
**In documentation:**
|
||||
- Create a document that marks which modules, classes, and interactions constitute the Core Domain
|
||||
- Use diagrams that distinguish core from supporting from generic
|
||||
- Keep this document updated as the model evolves
|
||||
|
||||
**In code:**
|
||||
- Organize the codebase so that Core Domain modules are clearly separated: `domain/core/`, `domain/supporting/`, `infrastructure/`
|
||||
- Use naming conventions that signal importance: a module called `pricing-engine` conveys more importance than `util-helpers`
|
||||
- Code review standards can be higher for core domain code (require domain expert sign-off)
|
||||
|
||||
### Distillation Techniques
|
||||
|
||||
#### Segregated Core
|
||||
|
||||
Physically separate the Core Domain from the rest of the codebase. The core should have no dependencies on supporting or generic subdomains -- only the other way around.
|
||||
|
||||
```
|
||||
src/
|
||||
core/ # Core Domain -- deepest modeling
|
||||
risk-assessment/
|
||||
RiskModel.py
|
||||
RiskFactor.py
|
||||
UnderwritingRules.py
|
||||
pricing/
|
||||
PricingEngine.py
|
||||
RateTable.py
|
||||
supporting/ # Supporting -- necessary, simpler design
|
||||
policy-admin/
|
||||
PolicyRepository.py
|
||||
PolicyDocument.py
|
||||
notifications/
|
||||
NotificationService.py
|
||||
generic/ # Generic -- thin wrappers around external services
|
||||
email/
|
||||
EmailGateway.py
|
||||
storage/
|
||||
FileStorage.py
|
||||
auth/
|
||||
AuthenticationAdapter.py
|
||||
```
|
||||
|
||||
#### Abstract Core
|
||||
|
||||
Create a distilled model that captures the essential abstractions of the Core Domain without the implementation details. This abstract model serves as a communication tool and a guide for detailed implementation.
|
||||
|
||||
The Abstract Core is like an executive summary of the domain model: it captures the key concepts, their relationships, and the most important business rules, without the full detail of every attribute and method.
|
||||
|
||||
## Build vs. Buy vs. Outsource
|
||||
|
||||
### Decision Framework
|
||||
|
||||
| Question | Core Domain | Supporting Subdomain | Generic Subdomain |
|
||||
|----------|-------------|---------------------|-------------------|
|
||||
| Should we build it in-house? | **Yes, always** | Yes, if no good fit exists | **No** |
|
||||
| Should we buy/use SaaS? | **No** -- too important to delegate | Only if it fits well | **Yes, always** |
|
||||
| Should we outsource development? | **No** -- requires deep domain expertise | Possible with good specs | **Yes** -- or better yet, buy |
|
||||
| Should we use open-source? | Only as a foundation to build on | Yes, if it fits | **Yes** |
|
||||
| What quality standard? | Highest -- deep modeling, extensive testing, expert review | Good -- solid engineering, adequate testing | Adequate -- it just needs to work |
|
||||
|
||||
### The Opportunity Cost Lens
|
||||
|
||||
Every hour spent on non-core work is an hour not spent on the Core Domain. Frame build-vs-buy decisions as opportunity costs:
|
||||
|
||||
- "We could build our own email service in 3 months." That is 3 months your best developers are not improving the Core Domain. Use SendGrid.
|
||||
- "We could build a custom monitoring dashboard in 6 weeks." That is 6 weeks not spent on the pricing engine. Use Grafana.
|
||||
- "We could build our own authentication system in 2 months." That is 2 months of security engineering not applied to fraud detection. Use Auth0.
|
||||
|
||||
### When "Buy" Becomes "Core"
|
||||
|
||||
Sometimes a generic subdomain becomes core as the business evolves:
|
||||
|
||||
- Stripe started as a payment processor (generic for most businesses) but made payments their Core Domain
|
||||
- Amazon started as a bookstore; logistics (supporting for most retailers) became a Core Domain that turned into AWS
|
||||
- Netflix treated content recommendation as core from the beginning, while most video platforms treated it as supporting
|
||||
|
||||
**Revisit classifications regularly.** What is generic today may become core tomorrow if the business strategy shifts. Annual or quarterly reviews of subdomain classifications prevent stale assumptions.
|
||||
|
||||
## Applying Strategic Design to Team Structure
|
||||
|
||||
### Team Allocation by Subdomain Type
|
||||
|
||||
| Subdomain Type | Team Characteristics | Practices |
|
||||
|---------------|---------------------|-----------|
|
||||
| Core Domain | Senior engineers, domain experts embedded in team, smallest and most skilled team | Deep modeling, event storming, pair programming with domain experts, extensive testing |
|
||||
| Supporting Subdomain | Mid-level engineers, domain knowledge acquired through documentation | Standard engineering practices, adequate testing, clear interfaces |
|
||||
| Generic Subdomain | Junior engineers or no team at all (use external service) | Integration work, adapter implementation, vendor management |
|
||||
|
||||
### Conway's Law Application
|
||||
|
||||
Design team boundaries to match desired bounded context boundaries:
|
||||
|
||||
- One team per bounded context (or small number of contexts)
|
||||
- Core Domain teams should be co-located or closely collaborating
|
||||
- Supporting Subdomain teams can be more independent
|
||||
- Generic Subdomain work can be distributed or handled by a platform team
|
||||
|
||||
### Investment Over Time
|
||||
|
||||
As the system matures, investment should shift:
|
||||
|
||||
| Phase | Core Domain Investment | Supporting Investment | Generic Investment |
|
||||
|-------|----------------------|----------------------|-------------------|
|
||||
| Early (MVP) | 70% | 20% | 10% (buy everything) |
|
||||
| Growth | 60% | 25% | 15% (integrate more) |
|
||||
| Mature | 50% | 30% | 20% (optimize and replace) |
|
||||
|
||||
The Core Domain always receives the plurality of investment. If it drops below 50%, the team is likely over-engineering supporting functionality or building generic functionality that should be bought.
|
||||
|
||||
## Strategic Design Anti-Patterns
|
||||
|
||||
| Anti-Pattern | Signal | Fix |
|
||||
|-------------|--------|-----|
|
||||
| "Golden hammer" -- applying Core Domain rigor to everything | Every module has aggregates, repositories, domain events, factories | Classify subdomains; simplify supporting and generic code |
|
||||
| "Platform first" -- building infrastructure before product | Months spent on logging, monitoring, and deployment before a single domain feature | Use off-the-shelf infrastructure; build Core Domain features first |
|
||||
| "Resume-driven development" -- choosing technology for novelty | Core Domain uses experimental framework because it is interesting | Choose boring technology for production; innovate in modeling, not infrastructure |
|
||||
| "Outsourced core" -- contracting out the competitive advantage | Core Domain built by offshore team with no domain expertise | Bring core development in-house; invest in domain expert access |
|
||||
| "Everyone is equal" -- same standards and investment everywhere | No distinction between a pricing algorithm and a CRUD admin panel | Apply deep modeling only where it pays off; keep the rest simple |
|
||||
End with a focused recommendation and its limitations. A strategy discussion does not authorize purchasing services, provisioning infrastructure, or reorganizing the codebase.
|
||||
@@ -1,202 +1,22 @@
|
||||
# Ubiquitous Language
|
||||
|
||||
The single most important practice in Domain-Driven Design. A ubiquitous language is the shared vocabulary between developers and domain experts that is used everywhere -- in conversation, documentation, code, tests, and diagrams. It is not a glossary appended to a wiki. It is the living, evolving language that shapes how the system is built and how the team thinks about the domain.
|
||||
Use this branch to resolve a terminology problem, not to rename every technical class. Read [the configured domain layout](../../../../docs/agents/domain.md) and the existing glossary before proposing terms.
|
||||
|
||||
## Why Language Matters More Than Code
|
||||
## Resolve the meaning
|
||||
|
||||
Software development is fundamentally a communication problem. The hardest bugs are not off-by-one errors or null pointer exceptions -- they are misunderstandings between people. When a developer hears "account" and thinks "user login record" while the domain expert means "financial ledger position," the resulting code will be structurally wrong in ways that no amount of testing can catch.
|
||||
Take a concrete business scenario and identify its actors, nouns, actions, and state transitions. Compare the user's explanation with the code and accepted decisions.
|
||||
|
||||
A ubiquitous language eliminates this class of failure by establishing a single, precise vocabulary that both sides use without translation. When the language is embedded in code, every class name, method name, and variable name becomes a checkpoint: if a domain expert cannot read the code and recognize the concepts, the model is wrong.
|
||||
- One term with different meanings: identify the actual distinction and its context.
|
||||
- Several terms for the same concept: propose one canonical term and record relevant aliases.
|
||||
- A lifecycle difference: decide whether it represents a state of one identity or a distinct concept.
|
||||
- An unfamiliar name: check domain usage before assuming the model is wrong.
|
||||
|
||||
### The Cost of Translation
|
||||
Ask for domain clarification only where the evidence does not settle a consequential choice. Do not invent an expert's agreement.
|
||||
|
||||
Without a ubiquitous language, every conversation requires mental translation:
|
||||
## Record and apply
|
||||
|
||||
| Developer Says | Domain Expert Hears | Actual Meaning | Risk |
|
||||
|---------------|--------------------|-|------|
|
||||
| `UserEntity` | "User? We don't have users, we have policyholders" | The system models a concept the business does not recognize | Code does not reflect reality; edge cases are missed |
|
||||
| `processData()` | "Process what data? Which business operation?" | A generic method hiding domain-specific logic | Business rules buried in implementation; impossible to validate |
|
||||
| `status = 3` | "What does 3 mean?" | An opaque encoding of a domain concept | Magic numbers replace meaningful domain states |
|
||||
| `ServiceManager` | "Manager of what?" | A catch-all class with no domain analog | God class accumulates unrelated responsibilities |
|
||||
Update root `CONTEXT.md` in its existing format when glossary maintenance is requested. Keep definitions concise, show important relationships, and distinguish agreed language from proposals. Technical names such as service or DTO may remain appropriate outside the domain glossary.
|
||||
|
||||
### The Payoff of Alignment
|
||||
A terminology discussion does not automatically authorize a code-wide rename. For an authorized rename, find all affected code, tests, schemas, contracts, and documents; replace them together under the development lifecycle policy. Do not introduce migration or API-version compatibility for disposable development data.
|
||||
|
||||
When the language is shared:
|
||||
|
||||
- **Domain experts can read tests.** A test that says `policy.underwrite(application)` is immediately meaningful. A test that says `service.process(dto)` is opaque.
|
||||
- **Developers catch domain errors.** When a developer writes `order.cancel()` and the domain expert says "we don't cancel orders, we void them," the naming mismatch reveals a modeling error.
|
||||
- **New team members onboard faster.** The codebase teaches the domain because the names are the domain.
|
||||
- **Refactoring is safer.** Renaming `process()` to `adjudicateClaim()` is not just cosmetic -- it encodes domain knowledge that prevents future misuse.
|
||||
|
||||
## Building the Language
|
||||
|
||||
### Start with Domain Expert Conversations
|
||||
|
||||
The language does not come from developers reading documentation. It comes from intensive, iterative conversations between developers and domain experts. These conversations follow a pattern:
|
||||
|
||||
1. **Listen for the nouns and verbs the expert uses naturally.** "When a claim comes in, the adjuster reviews it, and then we either approve or deny the claim." The nouns are `Claim`, `Adjuster`. The verbs are `review`, `approve`, `deny`.
|
||||
|
||||
2. **Challenge ambiguity.** "You said 'review' -- what exactly happens during a review? Is it the same as 'assess'?" Often the expert will distinguish between terms that outsiders conflate.
|
||||
|
||||
3. **Propose the model.** "So a `Claim` goes through an `Adjudication` process where an `Adjuster` either `approves` or `denies` it?" The expert corrects: "Not exactly -- the adjuster makes a recommendation, but only a senior adjuster can approve claims over $10,000."
|
||||
|
||||
4. **Refine until the model is precise.** The conversation reveals business rules that no requirements document captured: approval authority limits, recommendation vs. decision, escalation paths.
|
||||
|
||||
### Model Exploration Whirlpool
|
||||
|
||||
Eric Evans describes a "whirlpool" process for modeling:
|
||||
|
||||
1. **Scenario walkthrough.** Walk through concrete business scenarios with domain experts. "A customer calls to report damage to their vehicle. What happens next?"
|
||||
|
||||
2. **Concept extraction.** Identify the key domain concepts that emerge: `Claim`, `Incident`, `CoverageVerification`, `DamageAssessment`.
|
||||
|
||||
3. **Name negotiation.** Debate and agree on names. "Should we call it a 'damage report' or a 'claim'? When does a report become a claim?"
|
||||
|
||||
4. **Code spike.** Quickly implement the emerging model in code to test whether it holds up under real logic.
|
||||
|
||||
5. **Feedback loop.** Show the code (or at least the class and method names) back to the domain expert. "Does this look right to you?"
|
||||
|
||||
This cycle repeats continuously throughout the project, not just at the beginning.
|
||||
|
||||
## Language in Code
|
||||
|
||||
### Naming Classes After Domain Concepts
|
||||
|
||||
Every class in the domain layer should be named after a concept the business recognizes:
|
||||
|
||||
**Good names (domain language):**
|
||||
- `LoanApplication` -- the business knows what this is
|
||||
- `CreditDecision` -- an explicit outcome of an underwriting process
|
||||
- `PaymentSchedule` -- a concrete domain concept
|
||||
- `PolicyRenewalNotice` -- named exactly as the business document is named
|
||||
|
||||
**Bad names (technical language):**
|
||||
- `ApplicationDTO` -- DTO is a technical pattern, not a domain concept
|
||||
- `PaymentService` -- "service" is a technical role, not a domain concept; what does this service do?
|
||||
- `DataProcessor` -- meaningless in domain terms
|
||||
- `BaseEntityAbstractFactory` -- pure technical jargon
|
||||
|
||||
### Naming Methods After Domain Operations
|
||||
|
||||
Methods should read like sentences a domain expert would say:
|
||||
|
||||
**Good:**
|
||||
```
|
||||
policy.renew(effectiveDate)
|
||||
claim.submitForAdjudication()
|
||||
account.applyInterest(rate, period)
|
||||
order.fulfillWith(shipment)
|
||||
```
|
||||
|
||||
**Bad:**
|
||||
```
|
||||
policy.update(data)
|
||||
claim.process()
|
||||
account.calculate()
|
||||
order.setStatus(STATUS_SHIPPED)
|
||||
```
|
||||
|
||||
The difference is not cosmetic. `claim.submitForAdjudication()` tells you what the business operation is. `claim.process()` tells you nothing -- you must read the implementation to understand what it does, which means the domain knowledge is hidden.
|
||||
|
||||
### Naming Events After Domain Facts
|
||||
|
||||
Domain events should be named as past-tense facts that a domain expert would recognize as significant business occurrences:
|
||||
|
||||
**Good:** `PolicyIssued`, `ClaimDenied`, `PaymentOverdue`, `MembershipExpired`
|
||||
|
||||
**Bad:** `PolicyUpdated`, `ClaimProcessed`, `DataChanged`, `RecordModified`
|
||||
|
||||
"Updated," "processed," and "changed" are technical descriptions of what happened to data. "Issued," "denied," "overdue," and "expired" are domain descriptions of what happened in the business.
|
||||
|
||||
## Glossary Maintenance
|
||||
|
||||
### The Living Glossary
|
||||
|
||||
Maintain a glossary document that evolves with the model. This is not a static artifact created at the start of the project and forgotten. It is a living document updated every time the language changes.
|
||||
|
||||
**What goes in the glossary:**
|
||||
|
||||
| Term | Definition | Context | Example |
|
||||
|------|-----------|---------|---------|
|
||||
| Claim | A formal request for payment under an insurance policy following a covered event | Claims processing | "The policyholder filed a claim for water damage" |
|
||||
| Adjudication | The process of evaluating a claim to determine whether it is covered and how much to pay | Claims processing | "The claim is in adjudication pending the damage assessment" |
|
||||
| Coverage | The set of perils and limits defined in a policy | Underwriting | "This policy provides coverage for fire but not flood" |
|
||||
| Premium | The amount the policyholder pays for coverage | Billing | "The annual premium is $1,200, payable monthly" |
|
||||
|
||||
**What does NOT go in the glossary:**
|
||||
- Technical terms (`Repository`, `Service`, `Controller`)
|
||||
- Implementation details (`PostgreSQL table name`, `API endpoint`)
|
||||
- Generic programming concepts (`interface`, `abstract class`)
|
||||
|
||||
### When Terms Conflict
|
||||
|
||||
The same word often means different things in different parts of the business. This is expected and healthy:
|
||||
|
||||
- **"Account"** in billing means a financial ledger position. In authentication, it means a user login. In sales, it means a company relationship.
|
||||
- **"Product"** in the catalog means something you can browse. In inventory, it means a physical item with a location. In marketing, it means a brand offering.
|
||||
|
||||
The solution is not to force one definition. The solution is bounded contexts: each context has its own definition, and translations happen at the boundary.
|
||||
|
||||
## Language Evolution
|
||||
|
||||
### When to Change the Language
|
||||
|
||||
The ubiquitous language is not fixed. It evolves as the team's understanding of the domain deepens. Signals that the language needs to change:
|
||||
|
||||
- **Awkward conversations.** "Well, it's kind of like a customer but not exactly -- it's more of an applicant who might become a customer." This means `Customer` is the wrong term; `Applicant` is a distinct concept.
|
||||
- **Workarounds in code.** A `type` field that switches behavior (e.g., `if customer.type == 'prospect'`) often means a single class is trying to represent two different domain concepts.
|
||||
- **Expert correction.** "We don't really call it a 'request' -- we call it a 'submission.'" Change the code immediately.
|
||||
- **New domain insight.** "Actually, a cancellation and a voiding are different things. Cancellation is prospective; voiding is retroactive." Split the concept.
|
||||
|
||||
### The Refactoring Trigger
|
||||
|
||||
When the language changes, the code must change. This is not optional. If the team agrees that "submission" is the correct term instead of "request," then:
|
||||
|
||||
1. Rename the class: `Request` becomes `Submission`
|
||||
2. Rename the repository: `RequestRepository` becomes `SubmissionRepository`
|
||||
3. Rename the events: `RequestCreated` becomes `SubmissionFiled`
|
||||
4. Replace the development schema: `requests` becomes `submissions`; reseed disposable data under the repository lifecycle policy
|
||||
5. Replace the API contract: `/api/requests` becomes `/api/submissions`; update all callers and tests in the same change, without a legacy version
|
||||
|
||||
This may seem expensive, but the cost of maintaining a divergence between language and code is far higher. Every time a developer reads `Request` and has to mentally translate to "submission," they lose context and risk introducing errors.
|
||||
|
||||
## How Naming Shapes Design
|
||||
|
||||
Names are not labels applied after the fact. They are design decisions that constrain and guide the system's evolution.
|
||||
|
||||
### Names Reveal Missing Concepts
|
||||
|
||||
When you struggle to name something, the model is telling you something is wrong:
|
||||
|
||||
- **"OrderProcessorHelper"** -- If you need a helper, the class it is helping probably has the wrong boundaries. The behavior likely belongs inside `Order` or in a separate, well-named domain concept.
|
||||
- **"MiscService"** -- If you cannot name it, you do not understand it. Break it apart until each piece has a clear domain name.
|
||||
- **"DataValidator"** -- Validation of what? By what rules? `CreditApplicationValidator` or `AddressVerifier` tells you exactly what domain rules are being enforced.
|
||||
|
||||
### Names Prevent Misuse
|
||||
|
||||
A method named `account.debit(amount)` tells future developers exactly what this does and constrains its usage. A method named `account.update(amount)` invites misuse because "update" could mean anything.
|
||||
|
||||
### Names Create Boundaries
|
||||
|
||||
When two concepts share a name but have different behaviors, splitting the name splits the model:
|
||||
|
||||
- `Customer` in sales vs. `Customer` in billing? These are different bounded contexts. The split in naming reveals the split in models.
|
||||
- `Order` before payment vs. `Order` after payment? Perhaps these are `PendingOrder` and `ConfirmedOrder` -- two distinct states that deserve distinct types, not a `status` flag.
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
### The Jargon Trap
|
||||
|
||||
Developers invent internal jargon that has no domain analog: "We call it a 'widget' internally." If domain experts do not use the word "widget," it does not belong in the domain model.
|
||||
|
||||
### The Abbreviation Trap
|
||||
|
||||
`CustAcctMgr` is not a ubiquitous language term. Write `CustomerAccountManager` -- or better yet, ask what the business actually calls this role. Maybe it is a `RelationshipOfficer` or an `AccountExecutive`.
|
||||
|
||||
### The Thesaurus Trap
|
||||
|
||||
Using synonyms interchangeably (`customer` / `client` / `user` / `patron`) destroys precision. Pick one term per concept and enforce it everywhere. If the business uses "client" in legal contexts and "customer" in sales contexts, those are different bounded contexts with different terms -- and both are correct within their context.
|
||||
|
||||
### The Persistence Trap
|
||||
|
||||
Naming domain concepts after their storage mechanism: `CustomerRecord`, `OrderRow`, `PaymentTable`. Domain objects are not records or rows. They are domain concepts that happen to be persisted. Name them for what they are in the domain: `Customer`, `Order`, `Payment`.
|
||||
Different contexts may legitimately use different meanings. Do not split a coherent context merely because a word is ambiguous; first determine whether the underlying rules or ownership actually differ.
|
||||
@@ -33,6 +33,6 @@ Use this mode only when requested or when continuing an established learning wor
|
||||
|
||||
Reuse existing styles or widgets when available. Keep a new lesson self-contained unless actual reuse justifies extraction; do not scaffold a component library for hypothetical later lessons.
|
||||
|
||||
Add a reference sheet or glossary when it provides a useful reusable artifact. Keep it aligned with the lesson's terminology and link it from the relevant lesson. Use accessible semantics, readable layout, and keyboard-operable interactions.
|
||||
Add a reference sheet or glossary when it provides a useful reusable artifact. When recording learned terminology in the workspace glossary, read [GLOSSARY-FORMAT.md](GLOSSARY-FORMAT.md); distinguish it from a reference dictionary introducing new terms. Keep references aligned with lesson terminology and link them from the relevant lesson. Use accessible semantics, readable layout, and keyboard-operable interactions.
|
||||
|
||||
Hand off the lesson or artifact with its location and an appropriate next exercise. Do not open applications, commit files, or publish a course unless that is part of the requested workflow.
|
||||
@@ -1,201 +1,40 @@
|
||||
# Glossary — Building Great Skills
|
||||
|
||||
The domain model for what makes a skill great. A skill exists to wrangle determinism out of a stochastic system; the root virtue is **Predictability**, and every term below is a lever on it. This is the disclosed reference for [`writing-great-skills`](SKILL.md).
|
||||
|
||||
The terms are grouped by axis: **Invocation** (how a skill is reached), **Information Hierarchy** (how its content is arranged), **Steering** (how the agent's runtime behaviour is shaped), and **Pruning** (how it is kept lean). Each **failure mode** lives beside the lever that cures it, tagged _failure mode_.
|
||||
|
||||
**Bold terms** in any definition are themselves defined in this glossary; find them by their heading.
|
||||
|
||||
## Predictability
|
||||
|
||||
The degree to which a skill makes the agent behave the same _way_ on every run — the same process, not the same output (a brainstorming skill should _predictably_ diverge; its tokens vary, its behaviour doesn't). The root virtue every other term serves — cost and maintainability are symptoms of it, not rivals.
|
||||
|
||||
_Avoid_: consistency, reliability, robustness, output-determinism
|
||||
|
||||
## Invocation
|
||||
|
||||
How a skill is reached — and the two loads you pay for the choice.
|
||||
|
||||
### Model-Invoked
|
||||
|
||||
A skill available for automatic selection as well as explicit user invocation. This is Codex's default. Its required **description** explains the capability and when it applies; keep discovery concise. Use existing skills as shared reference only when they are available and relevant to the workflow.
|
||||
|
||||
_Avoid_: ability, tool, capability
|
||||
|
||||
### User-Invoked
|
||||
|
||||
A skill configured for explicit invocation with `policy.allow_implicit_invocation: false` in `agents/openai.yaml`. It still requires both `name` and **description**. Set this policy on a new skill only when the user requests it; preserve existing policies unless asked to change them. Discovery settings do not replace permission checks for actions.
|
||||
|
||||
_Avoid_: procedure, workflow, command
|
||||
|
||||
### Description
|
||||
|
||||
Required frontmatter summarizing what a skill does and when it applies. It is a **context pointer**, not an invocation-policy toggle. Never delete it to make a skill explicit-only; use the Codex policy setting instead.
|
||||
|
||||
_Avoid_: frontmatter, summary
|
||||
|
||||
### Context Pointer
|
||||
|
||||
A reference held in the agent's context that names some out-of-context material and encodes the condition for reaching it. The **description** is the top-level context pointer (context window → skill); pointers to disclosed files are the same object one level down. Its wording, not the target, decides _when_ the agent reaches — and _how reliably_. A must-have target behind a weakly worded pointer is a variance bug: fix the wording first, and inline the material only if sharpening fails.
|
||||
|
||||
_Avoid_: link, reference, import
|
||||
|
||||
### Context Load
|
||||
|
||||
The tokens and attention spent on available descriptions and loaded instructions. Keep descriptions discriminating and disclose conditional detail rather than duplicating it in every entrypoint.
|
||||
|
||||
_Avoid_: token cost, context bloat
|
||||
|
||||
### Cognitive Load
|
||||
|
||||
The cost a **user-invoked** skill imposes on the human — what they must hold in their head: which skills exist and when to reach for each (the human is the index). What **model-invocation** removes by being agent-discoverable, and the brake on splitting into more user-invoked skills. Not a cost to minimise: it is the price of human agency, the reason some skills stay user-invoked. Spend it where human judgement matters; remove it where it does not.
|
||||
|
||||
_Avoid_: human index, burden, overhead
|
||||
|
||||
### Router Skill
|
||||
|
||||
A skill that helps select among distinct workflows. Refer only to available skills or bundled resources, honor their invocation policies, and keep the user's requested destination authoritative. A simple skill with no meaningful branches needs no router.
|
||||
|
||||
_Avoid_: dispatcher, menu, registry, index, router procedure
|
||||
|
||||
### Granularity
|
||||
|
||||
How finely you divide skills. Finer division spends one of the two loads: more **model-invoked** skills spend **context load** (more descriptions crowding the window and competing for attention); more **user-invoked** skills spend **cognitive load** (more for the human to remember and reach for). Two cuts guide the division. By **invocation**, split off a model-invoked skill where you have a distinct **leading word** to trigger it — a trigger word you actually use in your prompts. By **sequence**, split a run of **steps** where a step's **post-completion steps** need hiding, since isolating it in its own context clears what follows. Beware the reverse: merging sequences exposes each step's post-completion steps to what follows, inviting premature completion.
|
||||
|
||||
_Avoid_: chunking, modularity
|
||||
|
||||
## Information Hierarchy
|
||||
|
||||
How a skill's content is arranged, and how far down the ladder each piece sits.
|
||||
|
||||
### Information Hierarchy
|
||||
|
||||
A skill's content ranked by how immediately the agent needs it — a single ladder, produced by two cuts: in-file or behind a pointer, and step or reference. The rungs:
|
||||
|
||||
- **Steps** — in-file, primary
|
||||
- **Reference**, in-file — secondary
|
||||
- **Reference**, disclosed — behind a **context pointer**
|
||||
|
||||
A skill with no **steps** uses just the bottom two rungs — often a legitimately flat peer-set (e.g. every rule of a review on one rung), which is a fine arrangement, not a smell. The hierarchy is independent of invocation: a skill can be model- or user-invoked whether it is all steps, all reference, or both. When a skill has steps, in-file reference that should be disclosed buries them and turns attending to them into a coin-flip — a variance lever, not just a legibility one. Keep the top of the ladder legible; push down it whatever you can.
|
||||
|
||||
_Avoid_: structure, organization, layout
|
||||
|
||||
### Steps
|
||||
|
||||
The ordered actions the agent performs — when a skill has them, the primary tier of its content, and the part that earns its place in SKILL.md. Not every skill has steps: a skill can be all steps (`tdd`), all **reference** (a review), or both, independent of invocation. Every step ends on a **completion criterion**, clear or vague.
|
||||
|
||||
_Avoid_: workflow, instructions, choreography
|
||||
|
||||
### Reference
|
||||
|
||||
Material the agent refers to on demand — definitions, facts, parameters, examples, conditional instructions. When a skill has **steps** it is secondary to them; when a skill has none it is the entire content; or it lives outside any skill entirely — see **External Reference**. Reached via **context pointers**, and the prime candidate for **progressive disclosure**.
|
||||
|
||||
_Avoid_: supporting material, docs, background
|
||||
|
||||
### External Reference
|
||||
|
||||
**Reference** in a plain file that can be read when a pointer's condition applies. Use it for shared material that does not need independent skill discovery; link it where needed rather than assuming other skills are available.
|
||||
|
||||
_Avoid_: doc, resource, knowledge base
|
||||
|
||||
### Progressive Disclosure
|
||||
|
||||
Moving **reference** down the ladder — out of SKILL.md and behind a **context pointer** — so the top stays legible. Not primarily a token optimisation; it is how the **information hierarchy** is protected. Licensed by **branching**: disclose what only some branches need, inline what every path needs, and if a pointer fires unreliably on must-have material, sharpen its wording, and pull it back inline only if that fails.
|
||||
|
||||
_Avoid_: lazy loading, chunking
|
||||
|
||||
### Co-location
|
||||
|
||||
Keeping the material an agent needs at once in one place — a concept's definition, rules, and caveats under a single heading, not scattered across the file — so reading one part brings its neighbours with it. The within-file companion to the **Information Hierarchy**: the hierarchy ranks _how far down_ a piece sits; co-location decides _what sits beside it_ once there. There is no formula for the right format of a body of **reference**; the test is that a skill should read like documentation written for the agent, and grouped material reads that way where scattered material does not. Distinct from **Duplication**: that repeats one meaning in two places, where scattering fragments a single meaning across many.
|
||||
|
||||
_Avoid_: grouping, clustering, cohesion
|
||||
|
||||
### Sprawl
|
||||
|
||||
_Failure mode._ A skill that is simply too long — too many lines in SKILL.md — independent of whether they are stale or repeated. Even an all-live, all-unique skill can sprawl. It costs readability (the agent wades through more before it can act, and attention thins across the excess), maintainability (every extra line is one more to keep **relevant**), and tokens. The cure is the **information hierarchy**: push **reference** down behind **context pointers**, and split by **branch** or sequence so each path carries only what it needs. Distinct from **sediment** (length from stale accumulation) and **duplication** (length from repeated meaning) — sprawl is length itself, whatever its cause.
|
||||
|
||||
_Avoid_: bloat, length, size, verbosity
|
||||
|
||||
## Steering
|
||||
|
||||
The levers that shape the agent's runtime behaviour toward **Predictability**.
|
||||
|
||||
### Branch
|
||||
|
||||
A distinct way a skill can be invoked — a case the skill handles — so different runs take different paths through it. A skill with many steps may carry many branches; a linear one has none.
|
||||
|
||||
_Avoid_: path, case, fork
|
||||
|
||||
### Leading Word
|
||||
|
||||
A compact concept — also called a _Leitwort_ — already living in the model's pretraining, that the agent thinks with while running the skill. It encodes a behavioural principle in the fewest possible tokens by invoking priors the model already holds (e.g. _lesson_, _proximal zone of development_, _fog of war_, _tracer bullets_). Repeated as a token, never as a sentence, it accumulates a distributed definition across the skill and anchors a whole region of behaviour. Coining your own works if you define it clearly, but a made-up word recruits no priors — you pay in definition tokens what a pretrained word gives free. Reach for an existing word first.
|
||||
|
||||
A leading word serves **predictability** twice. In the body it anchors **execution** — the agent reaches for the same behaviour every time the concept appears, and inside flat reference it focuses attention on a class of thing to look for, recruiting the right checks each run. In the **description** it anchors **invocation** — and not only within the skill: when the same word lives in your prompts, your docs, and your codebase, the agent links that shared language to the skill and fires it more reliably. Word a description with the leading words you actually use when you want the skill.
|
||||
|
||||
_Avoid_: keyword, term, motif
|
||||
|
||||
### Completion Criterion
|
||||
|
||||
The condition that tells the agent a unit of work is done — the target it judges against. Two properties make it a lever, not just a quality. Its **clarity** (can the agent tell done from not-done?) resists **premature completion** — a vague bound ("understanding reached") lets the agent declare done and slip to the next step; this axis needs _steps_ to bite, since premature completion is a between-steps failure. Its **demand** (how much it requires) sets **legwork** — "every modified model accounted for" forces thorough work where "produce a change list" does not — and this axis is _not_ step-bound: it can bind a body of flat reference too, which is how a skill with no steps still carries an exhaustiveness bar ("every rule applied"). The strongest criteria are both checkable and exhaustive.
|
||||
|
||||
_Avoid_: done condition, exit condition, stopping rule
|
||||
|
||||
### Legwork
|
||||
|
||||
The work an agent does behind the scenes within a single step — reading files, exploring the codebase, making changes, digging up what it needs rather than offloading to the user. It lives below the step structure: never written as its own step, latent in the wording, controlled by the agent rather than the skill. The within-step counterpart to **post-completion steps**' across-step pull. Raised by a **leading word** (_comprehensive_, _thorough_) or a **completion criterion** that demands the work be exhaustive — including the demand axis applied to flat reference, which is what drives a skill of flat reference to cover all its rungs. Goes thin either when that demand is missing or when **premature completion** cuts the step short.
|
||||
|
||||
_Avoid_: scope, effort, diligence, coverage
|
||||
|
||||
### Post-Completion Steps
|
||||
|
||||
The **steps** that follow the current step. Visible, they pull the agent forward into **premature completion** — the more it sees, the stronger the tug; the defence is to hide them by splitting the sequence of steps into two.
|
||||
|
||||
_Avoid_: horizon, fog of war, lookahead
|
||||
|
||||
### Premature Completion
|
||||
|
||||
_Failure mode._ Ending the current step before it is genuinely done, because the agent's attention slips to being done rather than to the work. A between-steps failure: it needs **steps** to occur — a skill with no steps that quits early isn't premature completion but thin **legwork** under an unmet demand. A tug-of-war between two forces: visible **post-completion steps** (the pull forward) and the **completion criterion**'s clarity (the resistance — a sharp, checkable bar holds; a vague one gives way). Fuzziness is the necessary condition: a sharp bound resists the pull no matter how many later steps are visible, so a step that never rushes needs no defending. Two levers hold a step that does, but reach for them in order: **sharpen the bound first** — it is local and cheap. Only when the criterion is irreducibly fuzzy _and_ you actually observe the rush do you **hide the later steps** — and hiding only works across a real context boundary (a user-invoked hand-off or a subagent dispatch; an inline model-invoked call leaves the later steps in context and clears nothing). One cause of thin legwork, but distinct from it: legwork can be thin even when a step runs to full completion.
|
||||
|
||||
_Avoid_: premature closure, the rush, rushing, shortcutting
|
||||
|
||||
### Negation
|
||||
|
||||
_Failure mode._ Steering by prohibition — telling the agent what _not_ to do — which drags the forbidden behaviour into context and makes it _more_ available, not less. _Don't think of an elephant_, and the elephant is all there is; _never write verbose comments_, and verbosity is the pattern the agent has just read. The negation is a weak modifier the strongly-activated concept overruns, so the ban half-reads as an instruction to do the thing. Its **leading word** is the _elephant_: whatever a prohibition names into the frame. Cure: prompt the **positive** — describe the target behaviour ("write one-line comments") so the banned one is never spoken. A prohibition earns its place only as a hard guardrail on a behaviour you cannot phrase positively; even then, pair it with the positive target so attention lands on what to do.
|
||||
|
||||
_Avoid_: ironic rebound, don't-prompting, the pink elephant
|
||||
|
||||
## Pruning
|
||||
|
||||
Keeping a skill lean — each remedy paired with the failure it cures.
|
||||
|
||||
### Single Source of Truth
|
||||
|
||||
The desired state where each meaning lives in exactly one authoritative place, so a change to the skill's behaviour is a change in one place. **Duplication** is its violation.
|
||||
|
||||
_Avoid_: home, canonical location
|
||||
|
||||
### Duplication
|
||||
|
||||
_Failure mode._ The same meaning given more than one **single source of truth**. It costs maintenance (change one place, you must change the others), costs tokens, and inflates prominence — repeating a meaning weights it on the ladder past its real rank. The accidental inverse of a **leading word**, which raises attention on purpose by repeating a token, never the meaning.
|
||||
|
||||
_Avoid_: repetition, redundancy
|
||||
|
||||
### Relevance
|
||||
|
||||
Whether a line still bears on what the skill does — the lens for what to keep. A line loses relevance either by never bearing on the task (mere exposition, or a **branch** that should be disclosed) or by going stale: drifting out of date as the behaviour or world it describes changes. Shorter skills are easier to keep relevant, because each line is cheaper to check. Distinct from **no-op**: relevance asks whether a line bears on the task, not whether it changes behaviour.
|
||||
|
||||
_Avoid_: load-bearing, staleness, freshness
|
||||
|
||||
### Sediment
|
||||
|
||||
_Failure mode._ Layers of old content that settle in a skill and are never cleared, because adding feels safe and removing feels risky — so stale and irrelevant lines accumulate and you must core down through them to find what is still live. The default fate of any skill without a pruning discipline; the slow erosion of **relevance**, as opposed to **duplication**'s repeated meaning.
|
||||
|
||||
_Avoid_: accretion, bloat, cruft, rot
|
||||
|
||||
### No-Op
|
||||
|
||||
_Failure mode._ An instruction that changes nothing because the model already does it by default — you pay load to tell the agent what it would do anyway. The test: does a line change behaviour versus the default? A line can be perfectly **relevant** and still be a no-op. The same priors that make a **leading word** free make a no-op worthless.
|
||||
|
||||
A leading word is a _technique_; No-Op is a _verdict_ on a line — and they cross. A leading word too weak to beat the default is a no-op (_be thorough_ when the agent is already thorough-ish), and the fix is a stronger word that passes the verdict (_relentless_), not a different technique. So the No-Op test — does it change behaviour versus the default? — is also how you grade whether a leading word is earning its repetitions. This is model-relative, not reader-relative: two people disagreeing over whether a line is a no-op disagree about the default, and settle it by running the skill, not by debate.
|
||||
|
||||
_Avoid_: redundant instruction, restating the obvious, belaboring
|
||||
# Instruction-Design Glossary
|
||||
|
||||
Use these terms when they clarify a discussion about agent instructions. They are concepts, not a requirement to avoid ordinary synonyms. [SKILL.md](SKILL.md) owns the workflow and Codex invocation mechanics.
|
||||
|
||||
## Discovery and cost
|
||||
|
||||
- **Predictability:** comparable situations lead to consistent, task-appropriate decisions.
|
||||
- **Model-invoked:** available for automatic selection as well as explicit invocation.
|
||||
- **User-invoked:** explicitly selected; an explicit-only policy disables automatic selection, not required metadata.
|
||||
- **Description:** required frontmatter that summarizes capability and applicability; it is not the invocation-policy toggle.
|
||||
- **Context pointer:** a reference plus the condition for reaching its target.
|
||||
- **Context load:** tokens and attention spent on available metadata and loaded instructions.
|
||||
- **Cognitive load:** what the human must remember or decide to use the system.
|
||||
- **Router skill:** selects among genuinely distinct workflows or resources.
|
||||
- **Granularity:** how finely instructions are divided; each split must justify its discovery and maintenance cost.
|
||||
|
||||
## Information hierarchy
|
||||
|
||||
- **Steps:** ordered actions where sequence matters to the task.
|
||||
- **Reference:** definitions, constraints, or procedures consulted when relevant.
|
||||
- **External reference:** material outside the entrypoint, reached through an appropriate pointer.
|
||||
- **Information hierarchy:** the placement of essential actions and constraints versus conditional detail.
|
||||
- **Progressive disclosure:** loading supporting detail only when the task needs it.
|
||||
- **Co-location:** keeping a concept's definition, rules, and caveats together.
|
||||
- **Branch:** a distinct case or mode handled by the skill.
|
||||
|
||||
## Execution and maintenance
|
||||
|
||||
- **Leading word:** a compact, familiar concept that conveys a useful decision principle without repeated explanation.
|
||||
- **Completion criterion:** evidence that the requested outcome has been reached; it should be checkable and cover the important requirements.
|
||||
- **Legwork:** investigation needed to meet that criterion rather than transferring discoverable work to the user.
|
||||
- **Post-completion steps:** later actions that may distract from an unfinished prerequisite.
|
||||
- **Premature completion:** declaring a step or task done without meeting its criterion. Clarify the criterion before adding process.
|
||||
- **Negation:** wording framed around what to avoid. Prefer a clear positive target where possible while retaining necessary safety prohibitions.
|
||||
- **Single source of truth:** one authoritative home for a rule or fact.
|
||||
- **Duplication:** repeated meaning that must be maintained in several places.
|
||||
- **Relevance:** whether material still supports the skill's actual task.
|
||||
- **Sediment:** stale assumptions accumulated through updates.
|
||||
- **Sprawl:** more instruction volume than the task usefully needs.
|
||||
- **No-op:** wording that does not materially change the agent's decisions or work.
|
||||
@@ -3,80 +3,38 @@ name: writing-great-skills
|
||||
description: Reference for writing and editing skills well — the vocabulary and principles that make a skill predictable.
|
||||
---
|
||||
|
||||
A skill exists to wrangle determinism out of a stochastic system. **Predictability** — the agent taking the same _process_ every run, not producing the same output — is the root virtue; every lever below serves it.
|
||||
# Writing Great Skills
|
||||
|
||||
**Bold terms** are defined in [`GLOSSARY.md`](GLOSSARY.md); look them up there for the full meaning.
|
||||
Write guidance that improves decisions while preserving the user's outcome, choices, and authority. Predictability means handling comparable situations consistently—not imposing the same ceremony on every task.
|
||||
|
||||
## Invocation
|
||||
## Codex mechanics
|
||||
|
||||
Every Codex skill requires YAML `name` and `description` fields, regardless of invocation mode. Keep the description concise and specific about the capability and when to use it.
|
||||
Every skill needs YAML `name` and `description`. Keep the description concise: what the skill does, when it applies, and a boundary only where it prevents likely misrouting.
|
||||
|
||||
- **Model-invoked** (automatic discovery) is the default; users can also invoke the skill explicitly.
|
||||
- For a new **user-invoked** (explicit-only) skill, set `policy.allow_implicit_invocation: false` in `agents/openai.yaml` only when the user requests that policy. Preserve an existing policy unless asked to change it.
|
||||
Automatic discovery is the default. Set `policy.allow_implicit_invocation: false` in `agents/openai.yaml` for a new explicit-only skill only when the user requests it. Preserve existing invocation policies and unrelated UI/dependency fields.
|
||||
|
||||
Do not use legacy `disable-model-invocation` or `argument-hint` frontmatter for Codex. Invocation policy does not grant authority: keep authorization checks at the actual mutation, not in discovery metadata. Preserve unrelated UI and dependency settings when updating metadata.
|
||||
Do not use legacy `disable-model-invocation` or `argument-hint` frontmatter for Codex, or delete descriptions to change invocation. Discovery policy does not grant authority; check authorization at the relevant mutation.
|
||||
|
||||
When user-invoked skills multiply past what you can remember, that piled-up cognitive load is cured by a **router skill**: one user-invoked skill that names the others and when to reach for each.
|
||||
## Write only decision-changing guidance
|
||||
|
||||
## Writing the description
|
||||
- State the intended outcome, non-obvious context, genuine constraints, and a recognizable completion condition.
|
||||
- Reuse explicit user choices and known context. Ask only for missing information that materially affects the task.
|
||||
- Match specificity to risk: fixed sequences belong in fragile workflows, not ordinary open-ended work.
|
||||
- Preserve authority boundaries. A review should not silently implement; a plan should not silently publish. Define stopping and retry behavior where failures or duplicate external writes matter.
|
||||
- Keep discovery specific. Avoid exhaustive synonym lists and generic triggers that attract unrelated tasks.
|
||||
|
||||
A **description** does two jobs — state what the skill does and when it applies. Keep only distinct **branches** that help select it; avoid exhaustive trigger lists. Descriptions earn even harder pruning than the body:
|
||||
## Arrange the information
|
||||
|
||||
- **Front-load the skill's leading word** — the description is where it does its invocation work.
|
||||
- **One trigger per branch.** Synonyms that rename a single branch are **duplication** — "build features using TDD … asks for test-first development" is one branch written twice. Collapse them; keep only genuinely distinct branches.
|
||||
- **Cut identity that's already in the body.** Keep the description to triggers, plus any "when another skill needs…" reach clause.
|
||||
Keep shared purpose and essential constraints in the entrypoint. Move substantial mode-specific procedures behind a link that says when to read them. Keep simple skills self-contained; add a router only for meaningful branches.
|
||||
|
||||
## Information hierarchy
|
||||
Use one authoritative home for each rule. Read the environment rather than copying readily discoverable configuration into instructions, except where a local gotcha is worth recording.
|
||||
|
||||
A skill is built from two content types — **steps** and **reference** — that mix freely: a skill can be all steps, all reference, or both. The core decision is which to use and where each sits on the **information hierarchy**, a ladder ranked by how immediately the agent needs the material:
|
||||
Use [GLOSSARY.md](GLOSSARY.md) when discussing instruction-design concepts such as context load, progressive disclosure, or premature completion. It defines the vocabulary; this file owns the authoring workflow and Codex mechanics.
|
||||
|
||||
1. **In-skill step** — an ordered action in `SKILL.md`, the primary tier: what the agent does, in order. Each step ends on a **completion criterion**, the condition that tells the agent the work is done. Make it _checkable_ (can the agent tell done from not-done?) and, where it matters, _exhaustive_ ("every modified model accounted for", not "produce a change list") — a vague criterion invites **premature completion**.
|
||||
2. **In-skill reference** — a definition, rule, or fact in `SKILL.md`, consulted on demand. Often a legitimately flat peer-set (every rule of a review on one rung) — a fine arrangement, not a smell. _This skill is all reference._
|
||||
3. **External reference** — reference pushed out of `SKILL.md` into a separate file, reached by a **context pointer**, loaded only when the pointer fires. (Spans _disclosed_ reference — a sibling file like `GLOSSARY.md`, still part of the skill — through fully **external reference** that lives outside the skill system and any skill can point at.)
|
||||
## Validate and prune
|
||||
|
||||
A demanding completion criterion drives thorough **legwork** — the digging the agent does within the work — whether the skill has steps or not, since "every rule applied" binds flat reference just as "every step done" binds a sequence.
|
||||
Check metadata with the target runtime's validator, confirm references are reachable, and run changed helpers with safe representative inputs. Test observable outcomes and invariants, not exact generated wording.
|
||||
|
||||
Push too little down and the top bloats; push too much and you hide material the agent actually needs. That tension is the whole decision.
|
||||
For complex or risky behavior, use a scoped independent evaluation when delegation is available and authorized. Keep fixtures isolated from user work and avoid live side effects.
|
||||
|
||||
**Progressive disclosure** is the move down the ladder — out of `SKILL.md` into a linked file — so the top stays legible. Mechanics: a linked `.md` file in the skill folder, named for what it holds (this skill discloses its full definitions to `GLOSSARY.md`). Some skills are used in more than one way, and each distinct way is a **branch** — different runs taking different paths through the skill. Branching is the cleanest disclosure test: inline what every branch needs, and push behind a pointer what only some branches reach. A **context pointer**'s _wording_, not its target, decides when and how reliably the agent reaches the material.
|
||||
|
||||
Where the ladder decides _how far down_ a piece sits, **co-location** decides _what sits beside it_ once there: keep a concept's definition, rules, and caveats under one heading rather than scattered, so reading one part brings its neighbours with it.
|
||||
|
||||
## When to split
|
||||
|
||||
**Granularity** is how finely you divide skills, and each cut spends one of the two loads, so split only when the cut earns it. Two cuts:
|
||||
|
||||
- **By invocation** — split off a **model-invoked** skill when you have a distinct **leading word** that should trigger it on its own, or another skill must reach it. You pay **context load** for the new always-loaded **description**, so that independent reach has to be worth it.
|
||||
- **By sequence** — split a run of **steps** when the steps still ahead (a step's **post-completion steps**) tempt the agent to rush the one in front of it (**premature completion**). Keeping them out of view encourages the agent to do more **legwork** on the current task.
|
||||
|
||||
## Pruning
|
||||
|
||||
Keep each meaning in a **single source of truth**: one authoritative place, so changing the behaviour is a one-place edit.
|
||||
|
||||
Check every line for **relevance**: does it still bear on what the skill does?
|
||||
|
||||
Then hunt **no-ops** sentence by sentence, not just line by line: run the no-op test on each sentence in isolation, and when one fails, delete the whole sentence rather than trim words from it. Be aggressive — most prose that fails should go, not be rewritten.
|
||||
|
||||
## Leading words
|
||||
|
||||
A **leading word** is a compact concept already living in the model's pretraining that the agent thinks with while running the skill (e.g. _lesson_, _fog of war_, _tracer bullets_). Repeated throughout the text (though not necessarily - a strong leading word might only be needed once), it accumulates a distributed definition and anchors a whole region of behaviour in the fewest tokens, by recruiting priors the model already holds.
|
||||
|
||||
It serves predictability twice. In the body it anchors _execution_: the agent reaches for the same behaviour every time the word appears. In the description it anchors _invocation_: when the same word lives in your prompts, docs, and code, the agent links that shared language to the skill and fires it more reliably.
|
||||
|
||||
Hunt for opportunities to refactor skills to use leading words. A triad spelled out at three sites (**duplication**), a description spending a sentence to gesture at one idea — each is a passage begging to **collapse** into a single token. Examples include:
|
||||
|
||||
- "fast, deterministic, low-overhead" -> _tight_ — one quality restated across a phase — into a single pretrained word (a _tight_ loop).
|
||||
- "a loop you believe in" -> _red_ — converts a fuzzy gate into a binary observable state (the loop goes _red_ on the bug, or it doesn't).
|
||||
|
||||
You win twice over: fewer tokens, _and_ a sharper hook for the agent to hang its thinking on. Assume every skill is carrying restatements that leading words retire — go find them.
|
||||
|
||||
## Failure modes
|
||||
|
||||
Use these to diagnose issues the user may be having with the skill.
|
||||
|
||||
- **Premature completion** — ending a step before it's genuinely done, attention slipping to _being done_. Defence, in order: sharpen the completion criterion first (cheap, local); only if it is irreducibly fuzzy _and_ you observe the rush, hide the post-completion steps by splitting (the sequence cut).
|
||||
- **Duplication** — the same meaning in more than one place. Costs maintenance and tokens, and inflates a meaning's prominence on the ladder past its real rank.
|
||||
- **Sediment** — stale layers that settle because adding feels safe and removing feels risky. The default fate of any skill without a pruning discipline.
|
||||
- **Sprawl** — a skill simply too long, even when every line is live and unique. Hurts readability and maintainability and wastes tokens. The cure is the ladder: disclose **reference** behind pointers, and split by **branch** or sequence so each path carries only what it needs.
|
||||
- **No-op** — a line the model already obeys by default, so you pay load to say nothing. The test: does it change behaviour versus the default? A weak leading word (_be thorough_ when the agent is already thorough-ish) is a no-op; the fix is a stronger word (_relentless_), not a different technique.
|
||||
- **Negation** — steering by prohibition backfires: _don't think of an elephant_ names the elephant and makes it more available, not less. Prompt the **positive** — state the target behaviour so the banned one is never spoken; keep a prohibition only as a hard guardrail you can't phrase positively, and even then pair it with what to do instead.
|
||||
Remove duplicated rules, stale assumptions, speculative scaffolding, and examples that do not clarify a real choice. Preserve safety and operational invariants even when shortening the text. If a task keeps failing, correct the demonstrated cause rather than adding universal ceremony.
|
||||
@@ -1,237 +1,99 @@
|
||||
## Agent skills
|
||||
|
||||
### Issue tracker
|
||||
|
||||
Issues and merge requests (MRs) are tracked in this repository's GitLab instance. See [issue-tracker.md](file:///home/nessar/lmnp-organizer/docs/agents/issue-tracker.md).
|
||||
|
||||
### Triage labels
|
||||
|
||||
Use the default triage labels (`needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `wontfix`). See [triage-labels.md](file:///home/nessar/lmnp-organizer/docs/agents/triage-labels.md).
|
||||
|
||||
### Domain docs
|
||||
|
||||
Use the single-context documentation layout. See [domain.md](file:///home/nessar/lmnp-organizer/docs/agents/domain.md).
|
||||
- Issues and merge requests use GitLab. For tracker operations, read [issue-tracker.md](docs/agents/issue-tracker.md).
|
||||
- Use the default triage labels (`needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `wontfix`); see [triage-labels.md](docs/agents/triage-labels.md).
|
||||
- Use the single-context domain layout: root `CONTEXT.md` and `docs/adr/`. Before exploring a domain, read [domain.md](docs/agents/domain.md) and relevant accepted ADRs.
|
||||
|
||||
## General engineering practices
|
||||
|
||||
- Make the smallest change that fully solves the issue.
|
||||
- Follow the existing project structure and conventions before introducing new patterns.
|
||||
- Prefer simple, explicit code over unnecessary abstraction.
|
||||
- Keep classes, components, and functions focused on one responsibility.
|
||||
- Extract reusable logic when it is duplicated or independently testable.
|
||||
- Avoid unrelated refactoring in feature or bug-fix merge requests.
|
||||
- Do not introduce a new dependency when the existing stack can reasonably solve the problem.
|
||||
- The application is not in production. Change development contracts and schemas directly when the domain model changes; update all callers and tests together instead of preserving backward compatibility.
|
||||
- Add or update tests for changed behavior.
|
||||
- Do not commit secrets, tokens, credentials, generated artifacts, or local environment files.
|
||||
- Update relevant domain documentation when behavior, architecture, or business rules change.
|
||||
|
||||
## Frontend: Angular
|
||||
|
||||
### Components
|
||||
|
||||
- Keep components small and focused on a single responsibility.
|
||||
- Split a component when a section has its own state, behavior, lifecycle, or reusable UI.
|
||||
- Keep page-level components focused on orchestration, routing, and layout.
|
||||
- Move reusable presentation elements into shared components.
|
||||
- Keep feature-specific components inside their feature directory.
|
||||
- Prefer composition over components with many configuration flags.
|
||||
- Avoid large component files. Components approaching 250 lines should be reviewed for extraction opportunities.
|
||||
- Do not place substantial business logic directly in templates or components.
|
||||
|
||||
### Angular architecture
|
||||
|
||||
- Organize code by feature rather than only by technical type.
|
||||
- Keep domain and business logic independent from Angular when practical.
|
||||
- Use services for API access, application coordination, and shared state.
|
||||
- Use dependency injection instead of manually constructing services.
|
||||
- Keep services focused; avoid catch-all services with unrelated responsibilities.
|
||||
- Prefer standalone components where they match the existing project architecture.
|
||||
- Lazy-load feature routes where appropriate.
|
||||
- Use route guards only for navigation concerns, not as the sole enforcement of security rules.
|
||||
- Follow the repository's established state-management approach. Do not introduce another state-management library without justification.
|
||||
|
||||
### Templates and state
|
||||
|
||||
- Keep templates readable and free of complex expressions.
|
||||
- Move non-trivial calculations into component methods, computed values, pipes, or domain utilities.
|
||||
- Do not call expensive or state-changing methods from templates.
|
||||
- Prefer Angular signals, observables, or local component state according to the existing codebase conventions.
|
||||
- Do not duplicate derived state. Compute values from their source whenever practical.
|
||||
- Avoid manual subscriptions when the `async` pipe, signals, or framework lifecycle helpers can manage them.
|
||||
- Ensure subscriptions are cleaned up when manual subscription is unavoidable.
|
||||
- Use `trackBy` or Angular's equivalent tracking syntax for dynamic lists.
|
||||
- Avoid unnecessary change-detection work.
|
||||
|
||||
### TypeScript
|
||||
|
||||
- Use strict TypeScript types.
|
||||
- Do not use `any` unless integration constraints make it unavoidable and the reason is documented.
|
||||
- Define explicit input, output, API, and domain types.
|
||||
- Prefer unions and enums over loosely related strings or multiple boolean flags.
|
||||
- Use immutable transformations where practical.
|
||||
- Do not use regular-expression `.test()` calls unless they are the only valid option and the user has explicitly authorized them; prefer more readable, maintainable validation alternatives.
|
||||
- Avoid non-null assertions unless the invariant is guaranteed and clear.
|
||||
- Validate data received from external APIs instead of trusting type assertions.
|
||||
|
||||
### Forms
|
||||
|
||||
- Prefer reactive forms for complex or business-critical forms.
|
||||
- Keep validation rules consistent between the frontend and backend.
|
||||
- Display actionable validation messages.
|
||||
- Prevent duplicate submissions while a request is pending.
|
||||
- Do not rely on frontend validation for security or data integrity.
|
||||
|
||||
### Styling and user experience
|
||||
|
||||
- Use Angular Material components by default whenever they cover the UI need.
|
||||
- Never use unstyled native buttons or browser-native dialogs; use Angular Material button directives and `MatDialog` instead.
|
||||
- Reuse the existing design system, shared components, spacing rules, and design tokens.
|
||||
- Keep all frontend color tokens and Material color mappings in `frontend/src/_palette.scss`; use its semantic CSS custom properties everywhere else and do not add raw hex, RGB, HSL, or OKLCH colors outside that file.
|
||||
- Keep all frontend component styles in `.scss` files; use Sass `@use` for shared styles and do not introduce new `.css` component styles.
|
||||
- Reuse `frontend/src/_tokens.scss` for spacing, radius, breakpoint, and container-query helpers; prefer Sass nesting, maps, functions, and mixins over repeated responsive or token-fallback declarations.
|
||||
- Avoid duplicating styles already provided by shared components.
|
||||
- Keep component styles scoped unless a style is intentionally global.
|
||||
- Ensure layouts work across supported viewport sizes.
|
||||
- Provide explicit loading, empty, success, and error states.
|
||||
- Every user-observable asynchronous frontend operation must expose a truthful loading lifecycle. Prefer content-shaped skeletons for first loads, preserve established content with subtle busy feedback during refreshes, and use operation-local progress for actions so unrelated work remains available; never present pending or failed data as an empty or successful result.
|
||||
- Avoid unexpected layout shifts and blocking interactions.
|
||||
|
||||
### Accessibility
|
||||
|
||||
- Use semantic HTML before adding ARIA attributes.
|
||||
- Use native buttons and links for interactive actions.
|
||||
- Ensure all interactive elements are keyboard accessible.
|
||||
- Associate form controls with visible or accessible labels.
|
||||
- Preserve visible focus indicators.
|
||||
- Provide alternative text for meaningful images.
|
||||
- Ensure dialogs and overlays manage focus correctly.
|
||||
- Do not communicate status or errors through color alone.
|
||||
|
||||
### Angular testing
|
||||
|
||||
- Test observable behavior rather than implementation details.
|
||||
- Add unit tests for components, services, pipes, guards, and utilities when their behavior changes.
|
||||
- Cover important form validation and error states.
|
||||
- Mock network boundaries rather than internal implementation details.
|
||||
- Add integration or end-to-end coverage for critical user journeys when appropriate.
|
||||
|
||||
## Backend: Java and Spring Boot
|
||||
|
||||
### Structure and responsibilities
|
||||
|
||||
- Keep controllers thin.
|
||||
- Controllers should handle HTTP concerns, input validation, authentication context, and response mapping.
|
||||
- Put application and business logic in dedicated services or use-case classes.
|
||||
- Keep persistence logic in repositories.
|
||||
- Do not expose persistence entities directly through public APIs.
|
||||
- Use dedicated request and response DTOs at API boundaries.
|
||||
- Keep mapping logic explicit and testable.
|
||||
- Organize code by domain or feature where possible.
|
||||
- Avoid utility classes that accumulate unrelated behavior.
|
||||
|
||||
### Java practices
|
||||
|
||||
- Follow the Java version and conventions already configured in the repository.
|
||||
- Prefer constructor injection over field injection.
|
||||
- Make dependencies explicit and immutable where possible.
|
||||
- Use descriptive names and small methods.
|
||||
- Avoid deeply nested conditionals; use guard clauses where they improve clarity.
|
||||
- Prefer domain-specific types over primitive strings and numbers for important concepts.
|
||||
- Use records for immutable data carriers when supported by the project.
|
||||
- Use enums for finite domain states.
|
||||
- Do not use `Optional` for entity fields, method parameters, or collection elements.
|
||||
- Return empty collections instead of `null`.
|
||||
- Do not catch broad exceptions unless they are rethrown, translated, or handled meaningfully.
|
||||
- Do not silently ignore exceptions.
|
||||
|
||||
### Spring Boot practices
|
||||
|
||||
- Use constructor-based dependency injection.
|
||||
- Keep configuration externalized through Spring configuration properties.
|
||||
- Use typed `@ConfigurationProperties` instead of scattering `@Value` annotations.
|
||||
- Keep environment-specific values outside source code.
|
||||
- Use profiles only for genuine environment differences.
|
||||
- Avoid circular dependencies.
|
||||
- Do not use static access to Spring-managed services.
|
||||
- Use framework annotations at the application boundary and keep domain code framework-independent where practical.
|
||||
|
||||
### API design
|
||||
|
||||
- Follow the repository's existing REST conventions.
|
||||
- Use nouns for resource paths and standard HTTP methods.
|
||||
- Return appropriate HTTP status codes.
|
||||
- Keep request and response contracts coherent with the current model; replace obsolete development contracts rather than adding compatibility layers.
|
||||
- Use explicit DTOs instead of generic maps.
|
||||
- Validate incoming requests with Jakarta Bean Validation.
|
||||
- Return consistent, structured error responses.
|
||||
- Do not expose stack traces, internal class names, database details, or sensitive information.
|
||||
- Document new or changed endpoints using the project's API documentation approach.
|
||||
- Consider pagination for endpoints returning potentially large collections.
|
||||
- Make retry-sensitive write operations idempotent when practical.
|
||||
|
||||
### Persistence and transactions
|
||||
|
||||
- Keep transaction boundaries in the service layer.
|
||||
- Use `@Transactional(readOnly = true)` for read-only service operations when appropriate.
|
||||
- Avoid long-running work, remote calls, or user interaction inside database transactions.
|
||||
- Prevent N+1 query problems.
|
||||
- Fetch only the data required for the use case.
|
||||
- Replace development schemas directly and reseed disposable data under the development data lifecycle below; do not add migration or compatibility paths.
|
||||
- Never rely on automatic schema generation in production.
|
||||
- Add indexes only when supported by an identified query pattern.
|
||||
- Preserve data integrity with database constraints in addition to application validation.
|
||||
- Handle concurrent updates explicitly where data consistency requires it.
|
||||
|
||||
### Security
|
||||
|
||||
- Enforce authorization on the backend for every protected operation.
|
||||
- Never rely on hidden frontend controls or route guards for authorization.
|
||||
- Validate and sanitize untrusted input.
|
||||
- Use parameterized queries or repository abstractions; never concatenate untrusted input into queries.
|
||||
- Do not log passwords, access tokens, session identifiers, personal data, or sensitive request payloads.
|
||||
- Store secrets in the configured secret-management or environment mechanism.
|
||||
- Use least-privilege access for services and database users.
|
||||
- Review new endpoints for authentication, authorization, data exposure, and abuse cases.
|
||||
|
||||
### Logging and observability
|
||||
|
||||
- Use structured, meaningful logs.
|
||||
- Log identifiers needed to investigate a request without exposing sensitive data.
|
||||
- Use appropriate log levels.
|
||||
- Do not use `System.out` or print stack traces directly.
|
||||
- Avoid duplicate logging of the same exception across multiple layers.
|
||||
- Preserve exception causes when translating errors.
|
||||
- Add metrics or tracing for important new workflows when the project already supports them.
|
||||
|
||||
### Spring Boot testing
|
||||
|
||||
- Add unit tests for domain and service logic.
|
||||
- Use integration tests for persistence, security, serialization, transactions, and framework configuration.
|
||||
- Prefer focused Spring test slices when the full application context is unnecessary.
|
||||
- Use the project's established database test strategy.
|
||||
- Do not depend on test execution order.
|
||||
- Keep tests deterministic and isolated.
|
||||
- Test success, validation failure, authorization failure, and important edge cases.
|
||||
- Add regression tests for bug fixes.
|
||||
- Make the smallest complete change; follow existing structure and conventions, keep responsibilities focused, and avoid unrelated refactoring.
|
||||
- Prefer simple, explicit code and composition. Extract logic when duplicated or independently testable; do not add abstractions or dependencies when the existing stack reasonably solves the problem.
|
||||
- Add or update tests for changed behavior, and relevant domain documentation for changed behavior, architecture, or business rules.
|
||||
- Preserve unrelated user changes. Never commit secrets, credentials, local environment files, debug output, temporary files, or generated artifacts.
|
||||
|
||||
## Development data lifecycle
|
||||
|
||||
- The application is not in production; development and test data are disposable and should be deleted or reseeded when the model changes.
|
||||
- Do not create data migrations, compatibility adapters, dual-read or dual-write paths, or legacy schema support.
|
||||
- Replace obsolete schemas, reference data, API contracts, and development documents directly, then update every caller and test in the same change.
|
||||
- Do not plan for rolling upgrades or backward-compatible deployment until an explicit project decision declares production data must be preserved.
|
||||
- Do not plan rolling upgrades or backward-compatible deployment until an explicit project decision declares production data must be preserved. Any future production schema management must be explicit, not automatic schema generation.
|
||||
|
||||
## Frontend: Angular
|
||||
|
||||
### Architecture and state
|
||||
|
||||
- Organize by feature. Keep pages focused on orchestration, routing, and layout; keep feature-specific components in their feature and reusable presentation in shared components.
|
||||
- Split sections that own state, behavior, lifecycle, or reusable UI. Review components approaching 250 lines for extraction; keep substantial business logic outside components and templates and independent of Angular when practical.
|
||||
- Use focused, injected services for API access, coordination, and shared state. Follow existing standalone-component and state-management conventions; justify any new state library. Lazy-load feature routes where appropriate.
|
||||
- Keep templates readable: move non-trivial calculations to methods, computed values, pipes, or domain utilities. Do not call expensive or state-changing methods from templates.
|
||||
- Compute derived state rather than duplicating it. Prefer existing signals, observables, or local state; use `async` or lifecycle helpers over manual subscriptions and clean up unavoidable subscriptions.
|
||||
- Track dynamic lists with `trackBy` or equivalent syntax and avoid unnecessary change-detection work.
|
||||
- Route guards address navigation only; backend authorization remains mandatory.
|
||||
|
||||
### TypeScript and forms
|
||||
|
||||
- Use strict types and explicit input, output, API, and domain contracts. Use `any` only for unavoidable integration constraints with a documented reason.
|
||||
- Prefer unions/enums over loosely related strings or boolean flags, and immutable transformations where practical. Avoid non-null assertions unless the invariant is guaranteed and clear.
|
||||
- Do not use regular-expression `.test()` calls unless they are the only valid option and the user has explicitly authorized them; prefer readable, maintainable validation alternatives.
|
||||
- Validate external API data instead of trusting assertions. Prefer reactive forms for complex or business-critical forms, keep validation consistent with the backend, and show actionable messages.
|
||||
- Prevent duplicate pending submissions. Frontend validation is not a security or integrity boundary.
|
||||
|
||||
### Styling, loading, and accessibility
|
||||
|
||||
- Use Angular Material by default where it covers the UI need. Use Material button directives on native buttons and `MatDialog` for dialogs; never use unstyled native buttons or browser-native dialogs.
|
||||
- Keep all frontend color tokens and Material mappings in `frontend/src/_palette.scss`. Use its semantic CSS custom properties elsewhere; no raw hex, RGB, HSL, or OKLCH colors outside that file.
|
||||
- Keep component styles in `.scss`, use Sass `@use` for shared styles, and do not introduce new `.css` component styles.
|
||||
- Reuse `frontend/src/_tokens.scss` for spacing, radius, breakpoint, and container-query helpers. Prefer Sass nesting, maps, functions, and mixins over repeated responsive or token-fallback declarations.
|
||||
- Reuse shared components and styles; scope component styles unless intentionally global. Support the required viewport sizes and avoid layout shifts or blocking interactions.
|
||||
- Provide explicit loading, empty, success, and error states. Every user-observable asynchronous operation must expose a truthful loading lifecycle. Prefer content-shaped skeletons for first loads, preserve content with subtle busy feedback for refreshes, and use operation-local progress for actions so unrelated work remains available. Never present pending or failed data as empty or successful.
|
||||
- Use semantic HTML before ARIA, native buttons/links for actions, keyboard access, accessible form labels, visible focus, meaningful image alternatives, and correct dialog/overlay focus management. Never communicate status or errors through color alone.
|
||||
|
||||
### Testing
|
||||
|
||||
- Test observable behavior, not implementation details, for changed components, services, pipes, guards, and utilities.
|
||||
- Cover important form validation, loading, and error states. Mock network boundaries, not internal implementation details; use integration/end-to-end coverage for critical journeys where appropriate.
|
||||
|
||||
## Backend: Java and Spring Boot
|
||||
|
||||
### Responsibilities and configuration
|
||||
|
||||
- Organize by domain or feature. Keep controllers thin: HTTP, input validation, authentication context, and response mapping. Put application/business logic in services or use cases and persistence in repositories.
|
||||
- Use explicit, testable mapping and dedicated API request/response DTOs, not persistence entities or generic maps.
|
||||
- Follow the configured Java version. Prefer constructor injection with explicit immutable dependencies; avoid field injection, circular dependencies, and static access to Spring-managed services.
|
||||
- Keep domain code framework-independent where practical, with framework annotations at application boundaries.
|
||||
- Externalize configuration through typed `@ConfigurationProperties`, not scattered `@Value` fields. Keep environment-specific values out of source and use profiles only for genuine environment differences.
|
||||
- Use domain-specific types, records for immutable data where supported, and enums for finite states. Keep methods focused and use guard clauses where they improve clarity.
|
||||
- Do not use `Optional` for entity fields, parameters, or collection elements; return empty collections instead of `null`.
|
||||
- Never silently ignore exceptions. Catch broad exceptions only when meaningfully handling, translating, or rethrowing them, and preserve their causes.
|
||||
|
||||
### API, persistence, and transactions
|
||||
|
||||
- Follow existing REST conventions: resource nouns, standard HTTP methods and appropriate status codes. Validate requests with Jakarta Bean Validation and return consistent structured errors.
|
||||
- Document endpoint changes using the existing API documentation approach. Consider pagination for large collections. Make retry-sensitive write operations idempotent when practical.
|
||||
- Keep transaction boundaries in services; use `@Transactional(readOnly = true)` for read operations where appropriate. Keep remote calls, long-running work, and user interaction outside transactions.
|
||||
- Fetch only needed data and prevent N+1 queries. Add indexes for identified query patterns, enforce integrity with database constraints as well as validation, and handle concurrent updates where consistency requires it.
|
||||
- Apply the development data lifecycle above to schema and API changes.
|
||||
|
||||
### Security and observability
|
||||
|
||||
- Enforce backend authorization on every protected operation; hidden controls and route guards are insufficient.
|
||||
- Validate and sanitize untrusted input. Use parameterized queries or repositories, never query concatenation with untrusted input.
|
||||
- Store secrets through configured environment/secret management and apply least privilege to services and database users.
|
||||
- Do not expose stack traces, internal class names, database details, or sensitive information in API responses.
|
||||
- Use structured logs with appropriate levels and useful non-sensitive identifiers. Do not log passwords, access tokens, session identifiers, personal data, or sensitive payloads.
|
||||
- Do not use `System.out`, print stack traces directly, or duplicate exception logging across layers. Add metrics/tracing for important workflows where already supported.
|
||||
- Review new endpoints for authentication, authorization, data exposure, and abuse cases.
|
||||
|
||||
### Testing
|
||||
|
||||
- Unit-test domain and service behavior. Use integration tests for persistence, security, serialization, transactions, and framework configuration, preferring focused Spring slices when sufficient.
|
||||
- Follow the established database test strategy. Keep tests deterministic, isolated, and order-independent.
|
||||
- Cover success, validation and authorization failures, important edge cases, and regressions for bug fixes.
|
||||
|
||||
## Before finishing
|
||||
|
||||
- Review the full diff for unrelated changes.
|
||||
- Verify that new code follows the existing architecture and naming conventions.
|
||||
- Check for oversized components, classes, and methods.
|
||||
- Check for duplicated logic and unnecessary abstractions.
|
||||
- Run the frontend formatter, linter, type checker, and relevant tests.
|
||||
- Run the backend formatter or style checks, static analysis, and relevant tests.
|
||||
- Run the project build when practical.
|
||||
- Verify API and database changes are reflected in tests and documentation.
|
||||
- Verify loading, empty, validation, error, and authorization states.
|
||||
- Confirm that no secrets, debug statements, temporary files, or generated artifacts were added.
|
||||
- Summarize any command that could not be run and explain why.
|
||||
- Review the full task diff for unrelated changes, architectural/naming consistency, oversized units, duplicated logic, unnecessary abstractions, and secret/debug/generated-file leakage.
|
||||
- For affected frontend behavior, run the formatter, linter, type checker, and relevant tests. For affected backend behavior, run formatting/style checks, static analysis, and relevant tests. Run the project build when practical.
|
||||
- For instruction, configuration, or helper changes, run their relevant validation and behavioral checks; application suites are needed when application behavior is affected.
|
||||
- Verify API/database changes are reflected in tests and documentation, and check loading, empty, validation, error, and authorization states where relevant.
|
||||
- Report checks run and any command not run or unavailable, with the reason.
|
||||
Reference in new issue
Block a user