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,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;
}
}
```