Fix Blossom CORS headers and add root-level upload routes (v0.36.12)
Some checks failed
Go / build-and-release (push) Has been cancelled

- Add proper CORS headers for Blossom endpoints including X-SHA-256,
  X-Content-Length, X-Content-Type headers required by blossom-client-sdk
- Add root-level Blossom routes (/upload, /media, /mirror, /report, /list/)
  for clients like Jumble that expect Blossom at root
- Export BaseURLKey from pkg/blossom for use by app handlers
- Make blossomRootHandler return URLs with /blossom prefix so blob
  downloads work via the registered /blossom/ route
- Remove Access-Control-Allow-Credentials header (not needed for * origin)
- Add Access-Control-Expose-Headers for X-Reason and other response headers

Files modified:
- app/blossom.go: Add blossomRootHandler, use exported BaseURLKey
- app/server.go: Add CORS handling for blossom paths, register root routes
- pkg/blossom/server.go: Fix CORS headers, export BaseURLKey
- pkg/blossom/utils.go: Minor formatting
- pkg/version/version: Bump to v0.36.12

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-24 11:32:52 +01:00
parent f326ff0307
commit c9a03db395
13 changed files with 4196 additions and 363 deletions

View File

@@ -0,0 +1,166 @@
---
name: domain-driven-design
description: This skill should be used when designing software architecture, modeling domains, reviewing code for DDD compliance, identifying bounded contexts, designing aggregates, or discussing strategic and tactical DDD patterns. Provides comprehensive Domain-Driven Design principles, axioms, heuristics, and anti-patterns for building maintainable, domain-centric software systems.
---
# Domain-Driven Design
## Overview
Domain-Driven Design (DDD) is an approach to software development that centers the design on the core business domain. This skill provides principles, patterns, and heuristics for both strategic design (system boundaries and relationships) and tactical design (code-level patterns).
## When to Apply This Skill
- Designing new systems or features with complex business logic
- Identifying and defining bounded contexts
- Modeling aggregates, entities, and value objects
- Reviewing code for DDD pattern compliance
- Decomposing monoliths into services
- Establishing ubiquitous language with domain experts
## Core Axioms
### Axiom 1: The Domain is Supreme
Software exists to solve domain problems. Technical decisions serve the domain, not vice versa. When technical elegance conflicts with domain clarity, domain clarity wins.
### Axiom 2: Language Creates Reality
The ubiquitous language shapes how teams think about the domain. Ambiguous language creates ambiguous software. Invest heavily in precise terminology.
### Axiom 3: Boundaries Enable Autonomy
Explicit boundaries (bounded contexts) allow teams to evolve independently. The cost of integration is worth the benefit of isolation.
### Axiom 4: Models are Imperfect Approximations
No model captures all domain complexity. Accept that models simplify reality. Refine models continuously as understanding deepens.
## Strategic Design Quick Reference
| Pattern | Purpose | Key Heuristic |
|---------|---------|---------------|
| **Bounded Context** | Define linguistic/model boundaries | One team, one language, one model |
| **Context Map** | Document context relationships | Make implicit integrations explicit |
| **Subdomain** | Classify domain areas by value | Core (invest), Supporting (adequate), Generic (outsource) |
| **Ubiquitous Language** | Shared vocabulary | If experts don't use the term, neither should code |
For detailed strategic patterns, consult `references/strategic-patterns.md`.
## Tactical Design Quick Reference
| Pattern | Purpose | Key Heuristic |
|---------|---------|---------------|
| **Entity** | Identity-tracked object | "Same identity = same thing" regardless of attributes |
| **Value Object** | Immutable, identity-less | Equality by value, always immutable, self-validating |
| **Aggregate** | Consistency boundary | Small aggregates, reference by ID, one transaction = one aggregate |
| **Domain Event** | Record state changes | Past tense naming, immutable, contains all relevant data |
| **Repository** | Collection abstraction | One per aggregate root, domain-focused interface |
| **Domain Service** | Stateless operations | When logic doesn't belong to any single entity |
| **Factory** | Complex object creation | When construction logic is complex or variable |
For detailed tactical patterns, consult `references/tactical-patterns.md`.
## Essential Heuristics
### Aggregate Design Heuristics
1. **Protect business invariants inside aggregate boundaries** - If two pieces of data must be consistent, they belong in the same aggregate
2. **Design small aggregates** - Large aggregates cause concurrency issues and slow performance
3. **Reference other aggregates by identity only** - Never hold direct object references across aggregate boundaries
4. **Update one aggregate per transaction** - Eventual consistency across aggregates using domain events
5. **Aggregate roots are the only entry point** - External code never reaches inside to manipulate child entities
### Bounded Context Heuristics
1. **Linguistic boundaries** - When the same word means different things, you have different contexts
2. **Team boundaries** - One context per team enables autonomy
3. **Process boundaries** - Different business processes often indicate different contexts
4. **Data ownership** - Each context owns its data; no shared databases
### Modeling Heuristics
1. **Nouns → Entities or Value Objects** - Things with identity become entities; descriptive things become value objects
2. **Verbs → Domain Services or Methods** - Actions become methods on entities or stateless services
3. **Business rules → Invariants** - Rules the domain must always satisfy become aggregate invariants
4. **Events in domain expert language → Domain Events** - "When X happens" becomes a domain event
## Decision Guides
### Entity vs Value Object
```
Does this thing have a lifecycle and identity that matters?
├─ YES → Is identity based on an ID (not attributes)?
│ ├─ YES → Entity
│ └─ NO → Reconsider; might be Value Object with natural key
└─ NO → Value Object
```
### Where Does This Logic Belong?
```
Is this logic stateless?
├─ NO → Does it belong to a single aggregate?
│ ├─ YES → Method on the aggregate/entity
│ └─ NO → Reconsider aggregate boundaries
└─ YES → Does it coordinate multiple aggregates?
├─ YES → Application Service
└─ NO → Does it represent a domain concept?
├─ YES → Domain Service
└─ NO → Infrastructure Service
```
### Should This Be a Separate Bounded Context?
```
Do different stakeholders use different language for this?
├─ YES → Separate bounded context
└─ NO → Does a different team own this?
├─ YES → Separate bounded context
└─ NO → Would a separate model reduce complexity?
├─ YES → Consider separation (but weigh integration cost)
└─ NO → Keep in current context
```
## Anti-Patterns Overview
| Anti-Pattern | Description | Fix |
|--------------|-------------|-----|
| **Anemic Domain Model** | Entities with only getters/setters | Move behavior into domain objects |
| **Big Ball of Mud** | No clear boundaries | Identify bounded contexts |
| **Smart UI** | Business logic in presentation layer | Extract domain layer |
| **Database-Driven Design** | Model follows database schema | Model follows domain, map to database |
| **Leaky Abstractions** | Infrastructure concerns in domain | Dependency inversion, ports and adapters |
| **God Aggregate** | One aggregate does everything | Split by invariant boundaries |
| **Premature Abstraction** | Abstracting before understanding | Concrete first, abstract when patterns emerge |
For detailed anti-patterns and remediation, consult `references/anti-patterns.md`.
## Implementation Checklist
When implementing DDD in a codebase:
- [ ] Ubiquitous language documented and used consistently in code
- [ ] Bounded contexts identified with clear boundaries
- [ ] Context map documenting integration patterns
- [ ] Aggregates designed small with clear invariants
- [ ] Entities have behavior, not just data
- [ ] Value objects are immutable and self-validating
- [ ] Domain events capture important state changes
- [ ] Repositories abstract persistence for aggregate roots
- [ ] No business logic in application services (orchestration only)
- [ ] No infrastructure concerns in domain layer
## Resources
### references/
- `strategic-patterns.md` - Detailed strategic DDD patterns including bounded contexts, context maps, subdomain classification, and ubiquitous language
- `tactical-patterns.md` - Detailed tactical DDD patterns including entities, value objects, aggregates, domain events, repositories, and services
- `anti-patterns.md` - Common DDD anti-patterns, how to identify them, and remediation strategies
To search references for specific topics:
- Bounded contexts: `grep -i "bounded context" references/`
- Aggregate design: `grep -i "aggregate" references/`
- Value objects: `grep -i "value object" references/`

View File

@@ -0,0 +1,853 @@
# DDD Anti-Patterns
This reference documents common anti-patterns encountered when implementing Domain-Driven Design, how to identify them, and remediation strategies.
## Anemic Domain Model
### Description
Entities that are mere data containers with getters and setters, while all business logic lives in "service" classes. The domain model looks like a relational database schema mapped to objects.
### Symptoms
- Entities with only get/set methods and no behavior
- Service classes with methods like `orderService.calculateTotal(order)`
- Business rules scattered across multiple services
- Heavy use of DTOs that mirror entity structure
- "Transaction scripts" in application services
### Example
```typescript
// ANTI-PATTERN: Anemic domain model
class Order {
id: string;
customerId: string;
items: OrderItem[];
status: string;
total: number;
// Only data access, no behavior
getId(): string { return this.id; }
setStatus(status: string): void { this.status = status; }
getItems(): OrderItem[] { return this.items; }
setTotal(total: number): void { this.total = total; }
}
class OrderService {
// All logic external to the entity
calculateTotal(order: Order): number {
let total = 0;
for (const item of order.getItems()) {
total += item.price * item.quantity;
}
order.setTotal(total);
return total;
}
canShip(order: Order): boolean {
return order.status === 'PAID' && order.getItems().length > 0;
}
ship(order: Order, trackingNumber: string): void {
if (!this.canShip(order)) {
throw new Error('Cannot ship order');
}
order.setStatus('SHIPPED');
order.trackingNumber = trackingNumber;
}
}
```
### Remediation
```typescript
// CORRECT: Rich domain model
class Order {
private _id: OrderId;
private _items: OrderItem[];
private _status: OrderStatus;
// Behavior lives in the entity
get total(): Money {
return this._items.reduce(
(sum, item) => sum.add(item.subtotal()),
Money.zero()
);
}
canShip(): boolean {
return this._status === OrderStatus.Paid && this._items.length > 0;
}
ship(trackingNumber: TrackingNumber): void {
if (!this.canShip()) {
throw new OrderNotShippableError(this._id, this._status);
}
this._status = OrderStatus.Shipped;
this._trackingNumber = trackingNumber;
}
addItem(item: OrderItem): void {
this.ensureCanModify();
this._items.push(item);
}
}
// Application service is thin - only orchestration
class OrderApplicationService {
async shipOrder(orderId: OrderId, trackingNumber: TrackingNumber): Promise<void> {
const order = await this.orderRepository.findById(orderId);
order.ship(trackingNumber); // Domain logic in entity
await this.orderRepository.save(order);
}
}
```
### Root Causes
- Developers treating objects as data structures
- Thinking in terms of database tables
- Copying patterns from CRUD applications
- Misunderstanding "service" to mean "all logic goes here"
## God Aggregate
### Description
An aggregate that has grown to encompass too much. It handles multiple concerns, has many child entities, and becomes a performance and concurrency bottleneck.
### Symptoms
- Aggregates with 10+ child entity types
- Long load times due to eager loading everything
- Frequent optimistic concurrency conflicts
- Methods that only touch a small subset of the aggregate
- Difficulty reasoning about invariants
### Example
```typescript
// ANTI-PATTERN: God aggregate
class Customer {
private _id: CustomerId;
private _profile: CustomerProfile;
private _addresses: Address[];
private _paymentMethods: PaymentMethod[];
private _orders: Order[]; // History of all orders!
private _wishlist: WishlistItem[];
private _reviews: Review[];
private _loyaltyPoints: LoyaltyAccount;
private _preferences: Preferences;
private _notifications: Notification[];
private _supportTickets: SupportTicket[];
// Loading this customer loads EVERYTHING
// Updating preferences causes concurrency conflict with order placement
}
```
### Remediation
```typescript
// CORRECT: Small, focused aggregates
class Customer {
private _id: CustomerId;
private _profile: CustomerProfile;
private _defaultAddressId: AddressId;
private _membershipTier: MembershipTier;
}
class CustomerAddressBook {
private _customerId: CustomerId;
private _addresses: Address[];
}
class ShoppingCart {
private _customerId: CustomerId; // Reference by ID
private _items: CartItem[];
}
class Wishlist {
private _customerId: CustomerId; // Reference by ID
private _items: WishlistItem[];
}
class LoyaltyAccount {
private _customerId: CustomerId; // Reference by ID
private _points: Points;
private _transactions: LoyaltyTransaction[];
}
```
### Identification Heuristic
Ask: "Do all these things need to be immediately consistent?" If the answer is no, they probably belong in separate aggregates.
## Aggregate Reference Violation
### Description
Aggregates holding direct object references to other aggregates instead of referencing by identity. Creates implicit coupling and makes it impossible to reason about transactional boundaries.
### Symptoms
- Navigation from one aggregate to another: `order.customer.address`
- Loading an aggregate brings in connected aggregates
- Unclear what gets saved when calling `save()`
- Difficulty implementing eventual consistency
### Example
```typescript
// ANTI-PATTERN: Direct reference
class Order {
private customer: Customer; // Direct reference!
private shippingAddress: Address;
getCustomerEmail(): string {
return this.customer.email; // Navigating through!
}
validate(): void {
// Touching another aggregate's data
if (this.customer.creditLimit < this.total) {
throw new Error('Credit limit exceeded');
}
}
}
```
### Remediation
```typescript
// CORRECT: Reference by identity
class Order {
private _customerId: CustomerId; // ID only!
private _shippingAddress: Address; // Value object copied at order time
// If customer data is needed, it must be explicitly loaded
static create(
customerId: CustomerId,
shippingAddress: Address,
creditLimit: Money // Passed in, not navigated to
): Order {
return new Order(customerId, shippingAddress, creditLimit);
}
}
// Application service coordinates loading if needed
class OrderApplicationService {
async getOrderWithCustomerDetails(orderId: OrderId): Promise<OrderDetails> {
const order = await this.orderRepository.findById(orderId);
const customer = await this.customerRepository.findById(order.customerId);
return new OrderDetails(order, customer);
}
}
```
## Smart UI
### Description
Business logic embedded directly in the user interface layer. Controllers, presenters, or UI components contain domain rules.
### Symptoms
- Validation logic in form handlers
- Business calculations in controllers
- State machines in UI components
- Domain rules duplicated across different UI views
- "If we change the UI framework, we lose the business logic"
### Example
```typescript
// ANTI-PATTERN: Smart UI
class OrderController {
submitOrder(request: Request): Response {
const cart = request.body;
// Business logic in controller!
let total = 0;
for (const item of cart.items) {
total += item.price * item.quantity;
}
// Discount rules in controller!
if (cart.items.length > 10) {
total *= 0.9; // 10% bulk discount
}
if (total > 1000 && !this.hasValidPaymentMethod(cart.customerId)) {
return Response.error('Orders over $1000 require verified payment');
}
// More business rules...
const order = {
customerId: cart.customerId,
items: cart.items,
total: total,
status: 'PENDING'
};
this.database.insert('orders', order);
return Response.ok(order);
}
}
```
### Remediation
```typescript
// CORRECT: UI delegates to domain
class OrderController {
submitOrder(request: Request): Response {
const command = new PlaceOrderCommand(
request.body.customerId,
request.body.items
);
try {
const orderId = this.orderApplicationService.placeOrder(command);
return Response.ok({ orderId });
} catch (error) {
if (error instanceof DomainError) {
return Response.badRequest(error.message);
}
throw error;
}
}
}
// Domain logic in domain layer
class Order {
private calculateTotal(): Money {
const subtotal = this._items.reduce(
(sum, item) => sum.add(item.subtotal()),
Money.zero()
);
return this._discountPolicy.apply(subtotal, this._items.length);
}
}
class BulkDiscountPolicy implements DiscountPolicy {
apply(subtotal: Money, itemCount: number): Money {
if (itemCount > 10) {
return subtotal.multiply(0.9);
}
return subtotal;
}
}
```
## Database-Driven Design
### Description
The domain model is derived from the database schema rather than from domain concepts. Tables become classes; foreign keys become object references; database constraints become business rules.
### Symptoms
- Class names match table names exactly
- Foreign key relationships drive object graph
- ID fields everywhere, even where identity doesn't matter
- `nullable` database columns drive optional properties
- Domain model changes require database migration first
### Example
```typescript
// ANTI-PATTERN: Database-driven model
// Mirrors database schema exactly
class orders {
order_id: number;
customer_id: number;
order_date: Date;
status_cd: string;
shipping_address_id: number;
billing_address_id: number;
total_amt: number;
tax_amt: number;
created_ts: Date;
updated_ts: Date;
}
class order_items {
order_item_id: number;
order_id: number;
product_id: number;
quantity: number;
unit_price: number;
discount_pct: number;
}
```
### Remediation
```typescript
// CORRECT: Domain-driven model
class Order {
private readonly _id: OrderId;
private _status: OrderStatus;
private _items: OrderItem[];
private _shippingAddress: Address; // Value object, not FK
private _billingAddress: Address;
// Domain behavior, not database structure
get total(): Money {
return this._items.reduce(
(sum, item) => sum.add(item.lineTotal()),
Money.zero()
);
}
ship(trackingNumber: TrackingNumber): void {
// Business logic
}
}
// Mapping is infrastructure concern
class OrderRepository {
async save(order: Order): Promise<void> {
// Map rich domain object to database tables
await this.db.query(
'INSERT INTO orders (id, status, shipping_street, shipping_city...) VALUES (...)'
);
}
}
```
### Key Principle
The domain model reflects how domain experts think, not how data is stored. Persistence is an infrastructure detail.
## Leaky Abstractions
### Description
Infrastructure concerns bleeding into the domain layer. Domain objects depend on frameworks, databases, or external services.
### Symptoms
- Domain entities with ORM decorators
- Repository interfaces returning database-specific types
- Domain services making HTTP calls
- Framework annotations on domain objects
- `import { Entity } from 'typeorm'` in domain layer
### Example
```typescript
// ANTI-PATTERN: Infrastructure leaking into domain
import { Entity, Column, PrimaryColumn, ManyToOne } from 'typeorm';
import { IsEmail, IsNotEmpty } from 'class-validator';
@Entity('customers') // ORM in domain!
export class Customer {
@PrimaryColumn()
id: string;
@Column()
@IsNotEmpty() // Validation framework in domain!
name: string;
@Column()
@IsEmail()
email: string;
@ManyToOne(() => Subscription) // ORM relationship in domain!
subscription: Subscription;
}
// Domain service calling external API directly
class ShippingCostService {
async calculateCost(order: Order): Promise<number> {
// HTTP call in domain!
const response = await fetch('https://shipping-api.com/rates', {
body: JSON.stringify(order)
});
return response.json().cost;
}
}
```
### Remediation
```typescript
// CORRECT: Clean domain layer
// Domain object - no framework dependencies
class Customer {
private constructor(
private readonly _id: CustomerId,
private readonly _name: CustomerName,
private readonly _email: Email
) {}
static create(name: string, email: string): Customer {
return new Customer(
CustomerId.generate(),
CustomerName.create(name), // Self-validating value object
Email.create(email) // Self-validating value object
);
}
}
// Port (interface) defined in domain
interface ShippingRateProvider {
getRate(destination: Address, weight: Weight): Promise<Money>;
}
// Domain service uses port
class ShippingCostCalculator {
constructor(private rateProvider: ShippingRateProvider) {}
async calculate(order: Order): Promise<Money> {
return this.rateProvider.getRate(
order.shippingAddress,
order.totalWeight()
);
}
}
// Adapter (infrastructure) implements port
class ShippingApiRateProvider implements ShippingRateProvider {
async getRate(destination: Address, weight: Weight): Promise<Money> {
const response = await fetch('https://shipping-api.com/rates', {
body: JSON.stringify({ destination, weight })
});
const data = await response.json();
return Money.of(data.cost, Currency.USD);
}
}
```
## Shared Database
### Description
Multiple bounded contexts accessing the same database tables. Changes in one context break others. No clear data ownership.
### Symptoms
- Multiple services querying the same tables
- Fear of schema changes because "something else might break"
- Unclear which service is authoritative for data
- Cross-context joins in queries
- Database triggers coordinating contexts
### Example
```typescript
// ANTI-PATTERN: Shared database
// Sales context
class SalesOrderService {
async getOrder(orderId: string) {
return this.db.query(`
SELECT o.*, c.name, c.email, p.name as product_name
FROM orders o
JOIN customers c ON o.customer_id = c.id
JOIN products p ON o.product_id = p.id
WHERE o.id = ?
`, [orderId]);
}
}
// Shipping context - same tables!
class ShippingService {
async getOrdersToShip() {
return this.db.query(`
SELECT o.*, c.address
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.status = 'PAID'
`);
}
async markShipped(orderId: string) {
// Directly modifying shared table
await this.db.query(
"UPDATE orders SET status = 'SHIPPED' WHERE id = ?",
[orderId]
);
}
}
```
### Remediation
```typescript
// CORRECT: Each context owns its data
// Sales context - owns order creation
class SalesOrderRepository {
async save(order: SalesOrder): Promise<void> {
await this.salesDb.query('INSERT INTO sales_orders...');
// Publish event for other contexts
await this.eventPublisher.publish(
new OrderPlaced(order.id, order.customerId, order.items)
);
}
}
// Shipping context - owns its projection
class ShippingOrderProjection {
// Handles events to build local projection
async handleOrderPlaced(event: OrderPlaced): Promise<void> {
await this.shippingDb.query(`
INSERT INTO shipments (order_id, customer_id, status)
VALUES (?, ?, 'PENDING')
`, [event.orderId, event.customerId]);
}
}
class ShipmentRepository {
async findPendingShipments(): Promise<Shipment[]> {
// Queries only shipping context's data
return this.shippingDb.query(
"SELECT * FROM shipments WHERE status = 'PENDING'"
);
}
}
```
## Premature Abstraction
### Description
Creating abstractions, interfaces, and frameworks before understanding the problem space. Often justified as "flexibility for the future."
### Symptoms
- Interfaces with single implementations
- Generic frameworks solving hypothetical problems
- Heavy use of design patterns without clear benefit
- Configuration systems for things that never change
- "We might need this someday"
### Example
```typescript
// ANTI-PATTERN: Premature abstraction
interface IOrderProcessor<TOrder, TResult> {
process(order: TOrder): Promise<TResult>;
}
interface IOrderValidator<TOrder> {
validate(order: TOrder): ValidationResult;
}
interface IOrderPersister<TOrder> {
persist(order: TOrder): Promise<void>;
}
abstract class AbstractOrderProcessor<TOrder, TResult>
implements IOrderProcessor<TOrder, TResult> {
constructor(
protected validator: IOrderValidator<TOrder>,
protected persister: IOrderPersister<TOrder>,
protected notifier: INotificationService,
protected logger: ILogger,
protected metrics: IMetricsCollector
) {}
async process(order: TOrder): Promise<TResult> {
this.logger.log('Processing order');
this.metrics.increment('orders.processed');
const validation = this.validator.validate(order);
if (!validation.isValid) {
throw new ValidationException(validation.errors);
}
const result = await this.doProcess(order);
await this.persister.persist(order);
await this.notifier.notify(order);
return result;
}
protected abstract doProcess(order: TOrder): Promise<TResult>;
}
// Only one concrete implementation ever created
class StandardOrderProcessor extends AbstractOrderProcessor<Order, OrderResult> {
protected async doProcess(order: Order): Promise<OrderResult> {
// The actual logic is trivial
return new OrderResult(order.id);
}
}
```
### Remediation
```typescript
// CORRECT: Concrete first, abstract when patterns emerge
class OrderService {
async placeOrder(command: PlaceOrderCommand): Promise<OrderId> {
const order = Order.create(command);
if (!order.isValid()) {
throw new InvalidOrderError(order.validationErrors());
}
await this.orderRepository.save(order);
return order.id;
}
}
// Only add abstraction when you have multiple implementations
// and understand the variation points
```
### Heuristic
Wait until you have three similar implementations before abstracting. The right abstraction will be obvious then.
## Big Ball of Mud
### Description
A system without clear architectural boundaries. Everything depends on everything. Changes ripple unpredictably.
### Symptoms
- No clear module boundaries
- Circular dependencies
- Any change might break anything
- "Only Bob understands how this works"
- Integration tests are the only reliable tests
- Fear of refactoring
### Identification
```
# Circular dependency example
OrderService → CustomerService → PaymentService → OrderService
```
### Remediation Strategy
1. **Identify implicit contexts** - Find clusters of related functionality
2. **Define explicit boundaries** - Create modules/packages with clear interfaces
3. **Break cycles** - Introduce events or shared kernel for circular dependencies
4. **Enforce boundaries** - Use architectural tests, linting rules
```typescript
// Step 1: Identify boundaries
// sales/ - order creation, pricing
// fulfillment/ - shipping, tracking
// customer/ - customer management
// shared/ - shared kernel (Money, Address)
// Step 2: Define public interfaces
// sales/index.ts
export { OrderService } from './application/OrderService';
export { OrderPlaced, OrderCancelled } from './domain/events';
// Internal types not exported
// Step 3: Break cycles with events
class OrderService {
async placeOrder(command: PlaceOrderCommand): Promise<OrderId> {
const order = Order.create(command);
await this.orderRepository.save(order);
// Instead of calling PaymentService directly
await this.eventPublisher.publish(new OrderPlaced(order));
return order.id;
}
}
class PaymentEventHandler {
async handleOrderPlaced(event: OrderPlaced): Promise<void> {
await this.paymentService.collectPayment(event.orderId, event.total);
}
}
```
## CRUD-Driven Development
### Description
Treating all domain operations as Create, Read, Update, Delete operations. Loses domain intent and behavior.
### Symptoms
- Endpoints like `PUT /orders/{id}` that accept any field changes
- Service methods like `updateOrder(orderId, updates)`
- Domain events named `OrderUpdated` instead of `OrderShipped`
- No validation of state transitions
- Business operations hidden behind generic updates
### Example
```typescript
// ANTI-PATTERN: CRUD-driven
class OrderController {
@Put('/orders/:id')
async updateOrder(id: string, body: Partial<Order>) {
// Any field can be updated!
return this.orderService.update(id, body);
}
}
class OrderService {
async update(id: string, updates: Partial<Order>): Promise<Order> {
const order = await this.repo.findById(id);
Object.assign(order, updates); // Blindly apply updates
return this.repo.save(order);
}
}
```
### Remediation
```typescript
// CORRECT: Intent-revealing operations
class OrderController {
@Post('/orders/:id/ship')
async shipOrder(id: string, body: ShipOrderRequest) {
return this.orderService.ship(id, body.trackingNumber);
}
@Post('/orders/:id/cancel')
async cancelOrder(id: string, body: CancelOrderRequest) {
return this.orderService.cancel(id, body.reason);
}
}
class OrderService {
async ship(orderId: OrderId, trackingNumber: TrackingNumber): Promise<void> {
const order = await this.repo.findById(orderId);
order.ship(trackingNumber); // Domain logic with validation
await this.repo.save(order);
await this.publish(new OrderShipped(orderId, trackingNumber));
}
async cancel(orderId: OrderId, reason: CancellationReason): Promise<void> {
const order = await this.repo.findById(orderId);
order.cancel(reason); // Validates cancellation is allowed
await this.repo.save(order);
await this.publish(new OrderCancelled(orderId, reason));
}
}
```
## Summary: Detection Checklist
| Anti-Pattern | Key Question |
|--------------|--------------|
| Anemic Domain Model | Do entities have behavior or just data? |
| God Aggregate | Does everything need immediate consistency? |
| Aggregate Reference Violation | Are aggregates holding other aggregates? |
| Smart UI | Would changing UI framework lose business logic? |
| Database-Driven Design | Does model match tables or domain concepts? |
| Leaky Abstractions | Does domain code import infrastructure? |
| Shared Database | Do multiple contexts write to same tables? |
| Premature Abstraction | Are there interfaces with single implementations? |
| Big Ball of Mud | Can any change break anything? |
| CRUD-Driven Development | Are operations generic updates or domain intents? |

View File

@@ -0,0 +1,358 @@
# Strategic DDD Patterns
Strategic DDD patterns address the large-scale structure of a system: how to divide it into bounded contexts, how those contexts relate, and how to prioritize investment across subdomains.
## Bounded Context
### Definition
A Bounded Context is an explicit boundary within which a domain model exists. Inside the boundary, all terms have specific, unambiguous meanings. The same term may mean different things in different bounded contexts.
### Why It Matters
- **Linguistic clarity** - "Customer" in Sales means something different than "Customer" in Shipping
- **Model isolation** - Changes to one model don't cascade across the system
- **Team autonomy** - Teams can work independently within their context
- **Focused complexity** - Each context solves one set of problems well
### Identification Heuristics
1. **Language divergence** - When stakeholders use the same word differently, there's a context boundary
2. **Department boundaries** - Organizational structure often mirrors domain structure
3. **Process boundaries** - End-to-end business processes often define context edges
4. **Data ownership** - Who is the authoritative source for this data?
5. **Change frequency** - Parts that change together should stay together
### Example: E-Commerce Platform
| Context | "Order" means... | "Product" means... |
|---------|------------------|-------------------|
| **Catalog** | N/A | Displayable item with description, images, categories |
| **Inventory** | N/A | Stock keeping unit with quantity and location |
| **Sales** | Shopping cart ready for checkout | Line item with price |
| **Fulfillment** | Shipment to be picked and packed | Physical item to ship |
| **Billing** | Invoice to collect payment | Taxable good |
### Implementation Patterns
#### Separate Deployables
Each bounded context as its own service/application.
```
catalog-service/
├── src/domain/Product.ts
└── src/infrastructure/CatalogRepository.ts
sales-service/
├── src/domain/Product.ts # Different model!
└── src/domain/Order.ts
```
#### Module Boundaries
Bounded contexts as modules within a monolith.
```
src/
├── catalog/
│ └── domain/Product.ts
├── sales/
│ └── domain/Product.ts # Different model!
└── shared/
└── kernel/Money.ts # Shared kernel
```
## Context Map
### Definition
A Context Map is a visual and documented representation of how bounded contexts relate to each other. It makes integration patterns explicit.
### Integration Patterns
#### Partnership
Two contexts develop together with mutual dependencies. Changes are coordinated.
```
┌─────────────┐ Partnership ┌─────────────┐
│ Catalog │◄──────────────────►│ Inventory │
└─────────────┘ └─────────────┘
```
**Use when**: Two teams must succeed or fail together.
#### Shared Kernel
A small, shared model that multiple contexts depend on. Changes require agreement from all consumers.
```
┌─────────────┐ ┌─────────────┐
│ Sales │ │ Billing │
└──────┬──────┘ └──────┬──────┘
│ │
└─────────► Money ◄──────────────┘
(shared kernel)
```
**Use when**: Core concepts genuinely need the same model.
**Danger**: Creates coupling. Keep shared kernels minimal.
#### Customer-Supplier
Upstream context (supplier) provides data/services; downstream context (customer) consumes. Supplier considers customer needs.
```
┌─────────────┐ ┌─────────────┐
│ Catalog │───── supplies ────►│ Sales │
│ (upstream) │ │ (downstream)│
└─────────────┘ └─────────────┘
```
**Use when**: One context clearly serves another, and the supplier is responsive.
#### Conformist
Downstream adopts upstream's model without negotiation. Upstream doesn't accommodate downstream needs.
```
┌─────────────┐ ┌─────────────┐
│ External │───── dictates ────►│ Our App │
│ API │ │ (conformist)│
└─────────────┘ └─────────────┘
```
**Use when**: Upstream won't change (third-party API), and their model is acceptable.
#### Anti-Corruption Layer (ACL)
Translation layer that protects a context from external models. Transforms data at the boundary.
```
┌─────────────┐ ┌───────┐ ┌─────────────┐
│ Legacy │───────►│ ACL │───────►│ New System │
│ System │ └───────┘ └─────────────┘
```
**Use when**: Upstream model would pollute downstream; translation is worth the cost.
```typescript
// Anti-Corruption Layer example
class LegacyOrderAdapter {
constructor(private legacyApi: LegacyOrderApi) {}
translateOrder(legacyOrder: LegacyOrder): Order {
return new Order({
id: OrderId.from(legacyOrder.order_num),
customer: this.translateCustomer(legacyOrder.cust_data),
items: legacyOrder.line_items.map(this.translateLineItem),
// Transform legacy status codes to domain concepts
status: this.mapStatus(legacyOrder.stat_cd),
});
}
private mapStatus(legacyCode: string): OrderStatus {
const mapping: Record<string, OrderStatus> = {
'OP': OrderStatus.Open,
'SH': OrderStatus.Shipped,
'CL': OrderStatus.Closed,
};
return mapping[legacyCode] ?? OrderStatus.Unknown;
}
}
```
#### Open Host Service
A context provides a well-defined protocol/API for others to consume.
```
┌─────────────┐
┌──────────►│ Reports │
│ └─────────────┘
┌───────┴───────┐ ┌─────────────┐
│ Catalog API │──►│ Search │
│ (open host) │ └─────────────┘
└───────┬───────┘ ┌─────────────┐
└──────────►│ Partner │
└─────────────┘
```
**Use when**: Multiple downstream contexts need access; worth investing in a stable API.
#### Published Language
A shared language format (schema) for communication between contexts. Often combined with Open Host Service.
Examples: JSON schemas, Protocol Buffers, GraphQL schemas, industry standards (HL7 for healthcare).
#### Separate Ways
Contexts have no integration. Each solves its needs independently.
**Use when**: Integration cost exceeds benefit; duplication is acceptable.
### Context Map Notation
```
┌───────────────────────────────────────────────────────────────┐
│ CONTEXT MAP │
├───────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ Partnership ┌─────────┐ │
│ │ Sales │◄────────────────────────────►│Inventory│ │
│ │ (U,D) │ │ (U,D) │ │
│ └────┬────┘ └────┬────┘ │
│ │ │ │
│ │ Customer/Supplier │ │
│ ▼ │ │
│ ┌─────────┐ │ │
│ │ Billing │◄──────────────────────────────────┘ │
│ │ (D) │ Conformist │
│ └─────────┘ │
│ │
│ Legend: U = Upstream, D = Downstream │
└───────────────────────────────────────────────────────────────┘
```
## Subdomain Classification
### Core Domain
The essential differentiator. This is where competitive advantage lives.
**Characteristics**:
- Unique to this business
- Complex, requires deep expertise
- Frequently changing as business evolves
- Worth significant investment
**Strategy**: Build in-house with best talent. Invest heavily in modeling.
### Supporting Subdomain
Necessary for the business but not a differentiator.
**Characteristics**:
- Important but not unique
- Moderate complexity
- Changes less frequently
- Custom implementation needed
**Strategy**: Build with adequate (not exceptional) investment. May outsource.
### Generic Subdomain
Solved problems with off-the-shelf solutions.
**Characteristics**:
- Common across industries
- Well-understood solutions exist
- Rarely changes
- Not a differentiator
**Strategy**: Buy or use open-source. Don't reinvent.
### Example: E-Commerce Platform
| Subdomain | Type | Strategy |
|-----------|------|----------|
| Product Recommendation Engine | Core | In-house, top talent |
| Inventory Management | Supporting | Build, adequate investment |
| Payment Processing | Generic | Third-party (Stripe, etc.) |
| User Authentication | Generic | Third-party or standard library |
| Shipping Logistics | Supporting | Build or integrate vendor |
| Customer Analytics | Core | In-house, strategic investment |
## Ubiquitous Language
### Definition
A common language shared by developers and domain experts. It appears in conversations, documentation, and code.
### Building Ubiquitous Language
1. **Listen to experts** - Use their terminology, not technical jargon
2. **Challenge vague terms** - "Process the order" → What exactly happens?
3. **Document glossary** - Maintain a living dictionary
4. **Enforce in code** - Class and method names use the language
5. **Refine continuously** - Language evolves with understanding
### Language in Code
```typescript
// Bad: Technical terms
class OrderProcessor {
handleOrderCreation(data: OrderData): void {
this.validateData(data);
this.persistToDatabase(data);
this.sendNotification(data);
}
}
// Good: Ubiquitous language
class OrderTaker {
placeOrder(cart: ShoppingCart): PlacedOrder {
const order = cart.checkout();
order.confirmWith(this.paymentGateway);
this.orderRepository.save(order);
this.domainEvents.publish(new OrderPlaced(order));
return order;
}
}
```
### Glossary Example
| Term | Definition | Context |
|------|------------|---------|
| **Order** | A confirmed purchase with payment collected | Sales |
| **Shipment** | Physical package(s) sent to fulfill an order | Fulfillment |
| **SKU** | Stock Keeping Unit; unique identifier for inventory | Inventory |
| **Cart** | Uncommitted collection of items a customer intends to buy | Sales |
| **Listing** | Product displayed for purchase in the catalog | Catalog |
### Anti-Pattern: Technical Language Leakage
```typescript
// Bad: Database terminology leaks into domain
order.setForeignKeyCustomerId(customerId);
order.persist();
// Bad: HTTP concerns leak into domain
order.deserializeFromJson(request.body);
order.setHttpStatus(200);
// Good: Domain language only
order.placeFor(customer);
orderRepository.save(order);
```
## Strategic Design Decisions
### When to Split a Bounded Context
Split when:
- Different parts need to evolve at different speeds
- Different teams need ownership
- Model complexity is becoming unmanageable
- Language conflicts are emerging within the context
Don't split when:
- Transaction boundaries would become awkward
- Integration cost outweighs isolation benefit
- Single team can handle the complexity
### When to Merge Bounded Contexts
Merge when:
- Integration overhead is excessive
- Same team owns both
- Models are converging naturally
- Separate contexts create artificial complexity
### Dealing with Legacy Systems
1. **Bubble context** - New bounded context with ACL to legacy
2. **Strangler fig** - Gradually replace legacy feature by feature
3. **Conformist** - Accept legacy model if acceptable
4. **Separate ways** - Rebuild independently, migrate data later

View File

@@ -0,0 +1,927 @@
# Tactical DDD Patterns
Tactical DDD patterns are code-level building blocks for implementing a rich domain model. They help express domain concepts in code that mirrors how domain experts think.
## Entity
### Definition
An object defined by its identity rather than its attributes. Two entities with the same attribute values but different identities are different things.
### Characteristics
- Has a unique identifier that persists through state changes
- Identity established at creation, immutable thereafter
- Equality based on identity, not attribute values
- Has a lifecycle (created, modified, potentially deleted)
- Contains behavior relevant to the domain concept it represents
### When to Use
- The object represents something tracked over time
- "Is this the same one?" is a meaningful question
- The object needs to be referenced from other parts of the system
- State changes are important to track
### Implementation
```typescript
// Entity with identity and behavior
class Order {
private readonly _id: OrderId;
private _status: OrderStatus;
private _items: OrderItem[];
private _shippingAddress: Address;
constructor(id: OrderId, items: OrderItem[], shippingAddress: Address) {
this._id = id;
this._items = items;
this._shippingAddress = shippingAddress;
this._status = OrderStatus.Pending;
}
get id(): OrderId {
return this._id;
}
// Behavior, not just data access
confirm(): void {
if (this._items.length === 0) {
throw new EmptyOrderError(this._id);
}
this._status = OrderStatus.Confirmed;
}
ship(trackingNumber: TrackingNumber): void {
if (this._status !== OrderStatus.Confirmed) {
throw new InvalidOrderStateError(this._id, this._status, 'ship');
}
this._status = OrderStatus.Shipped;
// Domain event raised
}
addItem(item: OrderItem): void {
if (this._status !== OrderStatus.Pending) {
throw new OrderModificationError(this._id);
}
this._items.push(item);
}
// Identity-based equality
equals(other: Order): boolean {
return this._id.equals(other._id);
}
}
// Strongly-typed identity
class OrderId {
constructor(private readonly value: string) {
if (!value || value.trim() === '') {
throw new InvalidOrderIdError();
}
}
equals(other: OrderId): boolean {
return this.value === other.value;
}
toString(): string {
return this.value;
}
}
```
### Entity vs Data Structure
```typescript
// Bad: Anemic entity (data structure)
class Order {
id: string;
status: string;
items: Item[];
// Only getters/setters, no behavior
}
// Good: Rich entity with behavior
class Order {
private _id: OrderId;
private _status: OrderStatus;
private _items: OrderItem[];
confirm(): void { /* enforces rules */ }
cancel(reason: CancellationReason): void { /* enforces rules */ }
addItem(item: OrderItem): void { /* enforces rules */ }
}
```
## Value Object
### Definition
An object defined entirely by its attributes. Two value objects with the same attributes are interchangeable. Has no identity.
### Characteristics
- Immutable - once created, never changes
- Equality based on attributes, not identity
- Self-validating - always in a valid state
- Side-effect free - methods return new instances
- Conceptually whole - attributes form a complete concept
### When to Use
- The concept has no lifecycle or identity
- "Are these the same?" means "do they have the same values?"
- Measurement, description, or quantification
- Combinations of attributes that belong together
### Implementation
```typescript
// Value Object: Money
class Money {
private constructor(
private readonly amount: number,
private readonly currency: Currency
) {}
// Factory method with validation
static of(amount: number, currency: Currency): Money {
if (amount < 0) {
throw new NegativeMoneyError(amount);
}
return new Money(amount, currency);
}
// Immutable operations - return new instances
add(other: Money): Money {
this.ensureSameCurrency(other);
return Money.of(this.amount + other.amount, this.currency);
}
subtract(other: Money): Money {
this.ensureSameCurrency(other);
return Money.of(this.amount - other.amount, this.currency);
}
multiply(factor: number): Money {
return Money.of(this.amount * factor, this.currency);
}
// Value-based equality
equals(other: Money): boolean {
return this.amount === other.amount &&
this.currency.equals(other.currency);
}
private ensureSameCurrency(other: Money): void {
if (!this.currency.equals(other.currency)) {
throw new CurrencyMismatchError(this.currency, other.currency);
}
}
}
// Value Object: Address
class Address {
private constructor(
readonly street: string,
readonly city: string,
readonly postalCode: string,
readonly country: Country
) {}
static create(street: string, city: string, postalCode: string, country: Country): Address {
if (!street || !city || !postalCode) {
throw new InvalidAddressError();
}
if (!country.validatePostalCode(postalCode)) {
throw new InvalidPostalCodeError(postalCode, country);
}
return new Address(street, city, postalCode, country);
}
// Returns new instance with modified value
withStreet(newStreet: string): Address {
return Address.create(newStreet, this.city, this.postalCode, this.country);
}
equals(other: Address): boolean {
return this.street === other.street &&
this.city === other.city &&
this.postalCode === other.postalCode &&
this.country.equals(other.country);
}
}
// Value Object: DateRange
class DateRange {
private constructor(
readonly start: Date,
readonly end: Date
) {}
static create(start: Date, end: Date): DateRange {
if (end < start) {
throw new InvalidDateRangeError(start, end);
}
return new DateRange(start, end);
}
contains(date: Date): boolean {
return date >= this.start && date <= this.end;
}
overlaps(other: DateRange): boolean {
return this.start <= other.end && this.end >= other.start;
}
durationInDays(): number {
return Math.floor((this.end.getTime() - this.start.getTime()) / (1000 * 60 * 60 * 24));
}
}
```
### Common Value Objects
| Domain | Value Objects |
|--------|--------------|
| **E-commerce** | Money, Price, Quantity, SKU, Address, PhoneNumber |
| **Healthcare** | BloodPressure, Dosage, DateRange, PatientId |
| **Finance** | AccountNumber, IBAN, TaxId, Percentage |
| **Shipping** | Weight, Dimensions, TrackingNumber, PostalCode |
| **General** | Email, URL, PhoneNumber, Name, Coordinates |
## Aggregate
### Definition
A cluster of entities and value objects with defined boundaries. Has an aggregate root entity that serves as the single entry point. External objects can only reference the root.
### Characteristics
- Defines a transactional consistency boundary
- Aggregate root is the only externally accessible object
- Enforces invariants across the cluster
- Loaded and saved as a unit
- Other aggregates referenced by identity only
### Design Rules
1. **Protect invariants** - All rules that must be consistent are inside the boundary
2. **Small aggregates** - Prefer single-entity aggregates; add children only when invariants require
3. **Reference by identity** - Never hold direct references to other aggregates
4. **Update one per transaction** - Eventual consistency between aggregates
5. **Design around invariants** - Identify what must be immediately consistent
### Implementation
```typescript
// Aggregate: Order (root) with OrderItems (child entities)
class Order {
private readonly _id: OrderId;
private _items: Map<ProductId, OrderItem>;
private _status: OrderStatus;
// Invariant: Order total cannot exceed credit limit
private _creditLimit: Money;
private constructor(
id: OrderId,
creditLimit: Money
) {
this._id = id;
this._items = new Map();
this._status = OrderStatus.Draft;
this._creditLimit = creditLimit;
}
static create(id: OrderId, creditLimit: Money): Order {
return new Order(id, creditLimit);
}
// All modifications go through aggregate root
addItem(productId: ProductId, quantity: Quantity, unitPrice: Money): void {
this.ensureCanModify();
const newItem = OrderItem.create(productId, quantity, unitPrice);
const projectedTotal = this.calculateTotalWith(newItem);
// Invariant enforcement
if (projectedTotal.isGreaterThan(this._creditLimit)) {
throw new CreditLimitExceededError(projectedTotal, this._creditLimit);
}
this._items.set(productId, newItem);
}
removeItem(productId: ProductId): void {
this.ensureCanModify();
this._items.delete(productId);
}
updateItemQuantity(productId: ProductId, newQuantity: Quantity): void {
this.ensureCanModify();
const item = this._items.get(productId);
if (!item) {
throw new ItemNotFoundError(productId);
}
const updatedItem = item.withQuantity(newQuantity);
const projectedTotal = this.calculateTotalWithUpdate(productId, updatedItem);
if (projectedTotal.isGreaterThan(this._creditLimit)) {
throw new CreditLimitExceededError(projectedTotal, this._creditLimit);
}
this._items.set(productId, updatedItem);
}
submit(): OrderSubmitted {
if (this._items.size === 0) {
throw new EmptyOrderError();
}
this._status = OrderStatus.Submitted;
return new OrderSubmitted(this._id, this.total(), new Date());
}
// Read-only access to child entities
get items(): ReadonlyArray<OrderItem> {
return Array.from(this._items.values());
}
total(): Money {
return this.items.reduce(
(sum, item) => sum.add(item.subtotal()),
Money.zero(Currency.USD)
);
}
private ensureCanModify(): void {
if (this._status !== OrderStatus.Draft) {
throw new OrderNotModifiableError(this._id, this._status);
}
}
private calculateTotalWith(newItem: OrderItem): Money {
return this.total().add(newItem.subtotal());
}
private calculateTotalWithUpdate(productId: ProductId, updatedItem: OrderItem): Money {
const currentItem = this._items.get(productId)!;
return this.total().subtract(currentItem.subtotal()).add(updatedItem.subtotal());
}
}
// Child entity - only accessible through aggregate root
class OrderItem {
private constructor(
private readonly _productId: ProductId,
private _quantity: Quantity,
private readonly _unitPrice: Money
) {}
static create(productId: ProductId, quantity: Quantity, unitPrice: Money): OrderItem {
return new OrderItem(productId, quantity, unitPrice);
}
get productId(): ProductId { return this._productId; }
get quantity(): Quantity { return this._quantity; }
get unitPrice(): Money { return this._unitPrice; }
subtotal(): Money {
return this._unitPrice.multiply(this._quantity.value);
}
withQuantity(newQuantity: Quantity): OrderItem {
return new OrderItem(this._productId, newQuantity, this._unitPrice);
}
}
```
### Aggregate Reference Patterns
```typescript
// Bad: Direct object reference across aggregates
class Order {
private customer: Customer; // Holds the entire aggregate!
}
// Good: Reference by identity
class Order {
private customerId: CustomerId;
// If customer data needed, load separately
getCustomerAddress(customerRepository: CustomerRepository): Address {
const customer = customerRepository.findById(this.customerId);
return customer.shippingAddress;
}
}
```
## Domain Event
### Definition
A record of something significant that happened in the domain. Captures state changes that domain experts care about.
### Characteristics
- Named in past tense (OrderPlaced, PaymentReceived)
- Immutable - records historical fact
- Contains all relevant data about what happened
- Published after state change is committed
- May trigger reactions in same or different bounded contexts
### When to Use
- Domain experts talk about "when X happens, Y should happen"
- Need to communicate changes across aggregate boundaries
- Maintaining an audit trail
- Implementing eventual consistency
- Integration with other bounded contexts
### Implementation
```typescript
// Base domain event
abstract class DomainEvent {
readonly occurredAt: Date;
readonly eventId: string;
constructor() {
this.occurredAt = new Date();
this.eventId = generateUUID();
}
abstract get eventType(): string;
}
// Specific domain events
class OrderPlaced extends DomainEvent {
constructor(
readonly orderId: OrderId,
readonly customerId: CustomerId,
readonly totalAmount: Money,
readonly items: ReadonlyArray<OrderItemSnapshot>
) {
super();
}
get eventType(): string {
return 'order.placed';
}
}
class OrderShipped extends DomainEvent {
constructor(
readonly orderId: OrderId,
readonly trackingNumber: TrackingNumber,
readonly carrier: string,
readonly estimatedDelivery: Date
) {
super();
}
get eventType(): string {
return 'order.shipped';
}
}
class PaymentReceived extends DomainEvent {
constructor(
readonly orderId: OrderId,
readonly amount: Money,
readonly paymentMethod: PaymentMethod,
readonly transactionId: string
) {
super();
}
get eventType(): string {
return 'payment.received';
}
}
// Entity raising events
class Order {
private _domainEvents: DomainEvent[] = [];
submit(): void {
// State change
this._status = OrderStatus.Submitted;
// Raise event
this._domainEvents.push(
new OrderPlaced(
this._id,
this._customerId,
this.total(),
this.itemSnapshots()
)
);
}
pullDomainEvents(): DomainEvent[] {
const events = [...this._domainEvents];
this._domainEvents = [];
return events;
}
}
// Event handler
class OrderPlacedHandler {
constructor(
private inventoryService: InventoryService,
private emailService: EmailService
) {}
async handle(event: OrderPlaced): Promise<void> {
// Reserve inventory (different aggregate)
await this.inventoryService.reserveItems(event.items);
// Send confirmation email
await this.emailService.sendOrderConfirmation(
event.customerId,
event.orderId,
event.totalAmount
);
}
}
```
### Event Publishing Patterns
```typescript
// Pattern 1: Collect and dispatch after save
class OrderApplicationService {
async placeOrder(command: PlaceOrderCommand): Promise<OrderId> {
const order = Order.create(command);
await this.orderRepository.save(order);
// Dispatch events after successful save
const events = order.pullDomainEvents();
await this.eventDispatcher.dispatchAll(events);
return order.id;
}
}
// Pattern 2: Outbox pattern (reliable publishing)
class OrderApplicationService {
async placeOrder(command: PlaceOrderCommand): Promise<OrderId> {
await this.unitOfWork.transaction(async () => {
const order = Order.create(command);
await this.orderRepository.save(order);
// Save events to outbox in same transaction
const events = order.pullDomainEvents();
await this.outbox.saveEvents(events);
});
// Separate process publishes from outbox
return order.id;
}
}
```
## Repository
### Definition
Mediates between the domain and data mapping layers. Provides collection-like interface for accessing aggregates.
### Characteristics
- One repository per aggregate root
- Interface defined in domain layer, implementation in infrastructure
- Returns fully reconstituted aggregates
- Abstracts persistence concerns from domain
### Interface Design
```typescript
// Domain layer interface
interface OrderRepository {
findById(id: OrderId): Promise<Order | null>;
save(order: Order): Promise<void>;
delete(order: Order): Promise<void>;
// Domain-specific queries
findPendingOrdersFor(customerId: CustomerId): Promise<Order[]>;
findOrdersToShipBefore(deadline: Date): Promise<Order[]>;
}
// Infrastructure implementation
class PostgresOrderRepository implements OrderRepository {
constructor(private db: Database) {}
async findById(id: OrderId): Promise<Order | null> {
const row = await this.db.query(
'SELECT * FROM orders WHERE id = $1',
[id.toString()]
);
if (!row) return null;
const items = await this.db.query(
'SELECT * FROM order_items WHERE order_id = $1',
[id.toString()]
);
return this.reconstitute(row, items);
}
async save(order: Order): Promise<void> {
await this.db.transaction(async (tx) => {
await tx.query(
'INSERT INTO orders (id, status, customer_id) VALUES ($1, $2, $3) ON CONFLICT (id) DO UPDATE SET status = $2',
[order.id.toString(), order.status, order.customerId.toString()]
);
// Save items
for (const item of order.items) {
await tx.query(
'INSERT INTO order_items (order_id, product_id, quantity, unit_price) VALUES ($1, $2, $3, $4) ON CONFLICT DO UPDATE...',
[order.id.toString(), item.productId.toString(), item.quantity.value, item.unitPrice.amount]
);
}
});
}
private reconstitute(orderRow: any, itemRows: any[]): Order {
// Rebuild aggregate from persistence data
return Order.reconstitute({
id: OrderId.from(orderRow.id),
status: OrderStatus[orderRow.status],
customerId: CustomerId.from(orderRow.customer_id),
items: itemRows.map(row => OrderItem.reconstitute({
productId: ProductId.from(row.product_id),
quantity: Quantity.of(row.quantity),
unitPrice: Money.of(row.unit_price, Currency.USD)
}))
});
}
}
```
### Repository vs DAO
```typescript
// DAO: Data-centric, returns raw data
interface OrderDao {
findById(id: string): Promise<OrderRow>;
findItems(orderId: string): Promise<OrderItemRow[]>;
insert(row: OrderRow): Promise<void>;
}
// Repository: Domain-centric, returns aggregates
interface OrderRepository {
findById(id: OrderId): Promise<Order | null>;
save(order: Order): Promise<void>;
}
```
## Domain Service
### Definition
Stateless operations that represent domain concepts but don't naturally belong to any entity or value object.
### When to Use
- The operation involves multiple aggregates
- The operation represents a domain concept
- Putting the operation on an entity would create awkward dependencies
- The operation is stateless
### Examples
```typescript
// Domain Service: Transfer money between accounts
class MoneyTransferService {
transfer(
from: Account,
to: Account,
amount: Money
): TransferResult {
// Involves two aggregates
// Neither account should "own" this operation
if (!from.canWithdraw(amount)) {
return TransferResult.insufficientFunds();
}
from.withdraw(amount);
to.deposit(amount);
return TransferResult.success(
new MoneyTransferred(from.id, to.id, amount)
);
}
}
// Domain Service: Calculate shipping cost
class ShippingCostCalculator {
constructor(
private rateProvider: ShippingRateProvider
) {}
calculate(
items: OrderItem[],
destination: Address,
shippingMethod: ShippingMethod
): Money {
const totalWeight = items.reduce(
(sum, item) => sum.add(item.weight),
Weight.zero()
);
const rate = this.rateProvider.getRate(
destination.country,
shippingMethod
);
return rate.calculateFor(totalWeight);
}
}
// Domain Service: Check inventory availability
class InventoryAvailabilityService {
constructor(
private inventoryRepository: InventoryRepository
) {}
checkAvailability(
items: Array<{ productId: ProductId; quantity: Quantity }>
): AvailabilityResult {
const unavailable: ProductId[] = [];
for (const { productId, quantity } of items) {
const inventory = this.inventoryRepository.findByProductId(productId);
if (!inventory || !inventory.hasAvailable(quantity)) {
unavailable.push(productId);
}
}
return unavailable.length === 0
? AvailabilityResult.allAvailable()
: AvailabilityResult.someUnavailable(unavailable);
}
}
```
### Domain Service vs Application Service
```typescript
// Domain Service: Domain logic, domain types, stateless
class PricingService {
calculateDiscountedPrice(product: Product, customer: Customer): Money {
const basePrice = product.price;
const discount = customer.membershipLevel.discountPercentage;
return basePrice.applyDiscount(discount);
}
}
// Application Service: Orchestration, use cases, transaction boundary
class OrderApplicationService {
constructor(
private orderRepository: OrderRepository,
private pricingService: PricingService,
private eventPublisher: EventPublisher
) {}
async createOrder(command: CreateOrderCommand): Promise<OrderId> {
const customer = await this.customerRepository.findById(command.customerId);
const order = Order.create(command.orderId, customer.id);
for (const item of command.items) {
const product = await this.productRepository.findById(item.productId);
const price = this.pricingService.calculateDiscountedPrice(product, customer);
order.addItem(item.productId, item.quantity, price);
}
await this.orderRepository.save(order);
await this.eventPublisher.publish(order.pullDomainEvents());
return order.id;
}
}
```
## Factory
### Definition
Encapsulates complex object or aggregate creation logic. Creates objects in a valid state.
### When to Use
- Construction logic is complex
- Multiple ways to create the same type of object
- Creation involves other objects or services
- Need to enforce invariants at creation time
### Implementation
```typescript
// Factory as static method
class Order {
static create(customerId: CustomerId, creditLimit: Money): Order {
return new Order(
OrderId.generate(),
customerId,
creditLimit,
OrderStatus.Draft,
[]
);
}
static reconstitute(data: OrderData): Order {
// For rebuilding from persistence
return new Order(
data.id,
data.customerId,
data.creditLimit,
data.status,
data.items
);
}
}
// Factory as separate class
class OrderFactory {
constructor(
private creditLimitService: CreditLimitService,
private idGenerator: IdGenerator
) {}
async createForCustomer(customerId: CustomerId): Promise<Order> {
const creditLimit = await this.creditLimitService.getLimit(customerId);
const orderId = this.idGenerator.generate();
return Order.create(orderId, customerId, creditLimit);
}
createFromQuote(quote: Quote): Order {
const order = Order.create(
this.idGenerator.generate(),
quote.customerId,
quote.creditLimit
);
for (const item of quote.items) {
order.addItem(item.productId, item.quantity, item.agreedPrice);
}
return order;
}
}
// Builder pattern for complex construction
class OrderBuilder {
private customerId?: CustomerId;
private items: OrderItemData[] = [];
private shippingAddress?: Address;
private billingAddress?: Address;
forCustomer(customerId: CustomerId): this {
this.customerId = customerId;
return this;
}
withItem(productId: ProductId, quantity: Quantity, price: Money): this {
this.items.push({ productId, quantity, price });
return this;
}
shippingTo(address: Address): this {
this.shippingAddress = address;
return this;
}
billingTo(address: Address): this {
this.billingAddress = address;
return this;
}
build(): Order {
if (!this.customerId) throw new Error('Customer required');
if (!this.shippingAddress) throw new Error('Shipping address required');
if (this.items.length === 0) throw new Error('At least one item required');
const order = Order.create(this.customerId);
order.setShippingAddress(this.shippingAddress);
order.setBillingAddress(this.billingAddress ?? this.shippingAddress);
for (const item of this.items) {
order.addItem(item.productId, item.quantity, item.price);
}
return order;
}
}
```