- 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>
13 KiB
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
- Language divergence - When stakeholders use the same word differently, there's a context boundary
- Department boundaries - Organizational structure often mirrors domain structure
- Process boundaries - End-to-end business processes often define context edges
- Data ownership - Who is the authoritative source for this data?
- 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.
// 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
- Listen to experts - Use their terminology, not technical jargon
- Challenge vague terms - "Process the order" → What exactly happens?
- Document glossary - Maintain a living dictionary
- Enforce in code - Class and method names use the language
- Refine continuously - Language evolves with understanding
Language in Code
// 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
// 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
- Bubble context - New bounded context with ACL to legacy
- Strangler fig - Gradually replace legacy feature by feature
- Conformist - Accept legacy model if acceptable
- Separate ways - Rebuild independently, migrate data later