Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Software architecture patterns skill from a 146-skill AI agent marketplace for Claude Code (Opus 4.6/Sonnet 4.6).
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/advanced-patterns.md
1# Advanced Architecture Patterns — Reference23Deep-dive implementation examples for DDD bounded contexts, Onion Architecture, Anti-Corruption Layers, and full project structures. Referenced from SKILL.md.45---67## Full Multi-Service Project Structure89A realistic e-commerce system organised by bounded context, each context is a deployable service:1011```12ecommerce/13├── services/14│ ├── identity/ # Bounded context: users & auth15│ │ ├── identity/16│ │ │ ├── domain/17│ │ │ │ ├── entities/18│ │ │ │ │ └── user.py19│ │ │ │ ├── value_objects/20│ │ │ │ │ ├── email.py21│ │ │ │ │ └── password_hash.py22│ │ │ │ └── interfaces/23│ │ │ │ └── user_repository.py24│ │ │ ├── use_cases/25│ │ │ │ ├── register_user.py26│ │ │ │ └── authenticate_user.py27│ │ │ ├── adapters/28│ │ │ │ ├── repositories/29│ │ │ │ │ └── postgres_user_repository.py30│ │ │ │ └── controllers/31│ │ │ │ └── auth_controller.py32│ │ │ └── infrastructure/33│ │ │ └── jwt_service.py34│ │ └── tests/35│ │ ├── unit/36│ │ └── integration/37│ │38│ ├── catalog/ # Bounded context: products39│ │ ├── catalog/40│ │ │ ├── domain/41│ │ │ │ ├── entities/42│ │ │ │ │ └── product.py43│ │ │ │ └── value_objects/44│ │ │ │ ├── sku.py45│ │ │ │ └── price.py46│ │ │ └── use_cases/47│ │ │ ├── create_product.py48│ │ │ └── update_inventory.py49│ │ └── tests/50│ │51│ └── ordering/ # Bounded context: orders52│ ├── ordering/53│ │ ├── domain/54│ │ │ ├── entities/55│ │ │ │ └── order.py56│ │ │ ├── value_objects/57│ │ │ │ ├── customer_id.py # NOT imported from identity!58│ │ │ │ └── money.py59│ │ │ └── interfaces/60│ │ │ ├── order_repository.py61│ │ │ └── catalog_client.py # ACL port to catalog context62│ │ ├── use_cases/63│ │ │ ├── place_order.py64│ │ │ └── cancel_order.py65│ │ └── adapters/66│ │ ├── acl/67│ │ │ └── catalog_http_client.py # ACL adapter68│ │ └── repositories/69│ │ └── postgres_order_repository.py70│ └── tests/71│72├── shared/ # Shared kernel (use sparingly)73│ └── domain_events/74│ └── base_event.py75└── docker-compose.yml76```7778---7980## Onion Architecture vs. Clean Architecture8182Both enforce inward-pointing dependencies. The difference is terminology and layering granularity:8384| Concern | Clean Architecture | Onion Architecture |85|---|---|---|86| Innermost ring | Entities | Domain Model |87| Second ring | Use Cases | Domain Services |88| Third ring | Interface Adapters | Application Services |89| Outermost ring | Frameworks & Drivers | Infrastructure / UI / Tests |90| Key insight | Controller is an adapter | Application Services = Use Cases |9192Onion Architecture makes the Domain Services layer explicit — it hosts pure domain logic that spans multiple entities but has no I/O:9394```python95# onion/domain/services/pricing_service.py96from domain.entities.product import Product97from domain.value_objects.money import Money98from domain.value_objects.discount import Discount99100class PricingService:101"""102Domain service: logic that doesn't belong to a single entity.103No ports or adapters here — purely domain computation.104"""105106def apply_bulk_discount(self, product: Product, quantity: int) -> Money:107if quantity >= 100:108discount = Discount(percentage=20)109elif quantity >= 50:110discount = Discount(percentage=10)111else:112discount = Discount(percentage=0)113return product.price.apply_discount(discount)114115def calculate_order_total(self, items: list[tuple[Product, int]]) -> Money:116subtotals = [self.apply_bulk_discount(p, q) for p, q in items]117return sum(subtotals[1:], subtotals[0]) if subtotals else Money(0, "USD")118```119120---121122## Anti-Corruption Layer (ACL)123124When the `Ordering` context must fetch product data from the `Catalog` context, it should never use `Catalog`'s domain model directly. An ACL translates between the two models:125126```python127# ordering/domain/interfaces/catalog_client.py128from abc import ABC, abstractmethod129from ordering.domain.value_objects.product_snapshot import ProductSnapshot130131class CatalogClientPort(ABC):132"""133Ordering's view of product data. Uses Ordering's own value object,134not Catalog's Product entity.135"""136137@abstractmethod138async def get_product_snapshot(self, sku: str) -> ProductSnapshot: ...139140141# ordering/domain/value_objects/product_snapshot.py142from dataclasses import dataclass143from ordering.domain.value_objects.money import Money144145@dataclass(frozen=True)146class ProductSnapshot:147"""Ordering's local representation of a product at order time."""148sku: str149name: str150unit_price: Money151available: bool152153154# ordering/adapters/acl/catalog_http_client.py155import httpx156from ordering.domain.interfaces.catalog_client import CatalogClientPort157from ordering.domain.value_objects.product_snapshot import ProductSnapshot158from ordering.domain.value_objects.money import Money159160class CatalogHttpClient(CatalogClientPort):161"""162ACL adapter: calls Catalog's HTTP API and translates163Catalog's response schema into Ordering's ProductSnapshot.164"""165166def __init__(self, base_url: str, http_client: httpx.AsyncClient):167self._base_url = base_url168self._http = http_client169170async def get_product_snapshot(self, sku: str) -> ProductSnapshot:171response = await self._http.get(f"{self._base_url}/products/{sku}")172response.raise_for_status()173data = response.json()174175# Translation: Catalog speaks "price_cents" + "currency_code";176# Ordering speaks Money(amount, currency).177return ProductSnapshot(178sku=data["sku"],179name=data["title"], # field name differs between contexts180unit_price=Money(181amount=data["price_cents"],182currency=data["currency_code"],183),184available=data["stock_count"] > 0,185)186187188# Test ACL with a stub — no HTTP required189class StubCatalogClient(CatalogClientPort):190def __init__(self, products: dict[str, ProductSnapshot]):191self._products = products192193async def get_product_snapshot(self, sku: str) -> ProductSnapshot:194if sku not in self._products:195raise ValueError(f"Unknown SKU: {sku}")196return self._products[sku]197```198199---200201## Context Map — Relationships Between Bounded Contexts202203```204┌─────────────────────────────────────────────────────────────────┐205│ E-Commerce System │206│ │207│ ┌─────────────┐ Open Host ┌─────────────────────────┐ │208│ │ Identity │──────────────▶│ Ordering │ │209│ │ Context │ │ (uses CustomerId VO, │ │210│ │ │ │ not User entity) │ │211│ └─────────────┘ └─────────────────────────┘ │212│ │ ACL │213│ ▼ │214│ ┌─────────────────┐ │215│ ┌─────────────┐ Shared │ Catalog │ │216│ │ Payments │ Kernel │ Context │ │217│ │ Context │◀─────────────▶│ │ │218│ │ │ (Money VO) └─────────────────┘ │219│ └─────────────┘ │220└─────────────────────────────────────────────────────────────────┘221222Relationship types:223Open Host Service — upstream provides a stable API for many downstream contexts224ACL (Anti-Corruption Layer) — downstream translates upstream model to its own225Shared Kernel — two contexts share a small, explicitly governed sub-model226Conformist — downstream adopts upstream model as-is (last resort)227```228229---230231## Dependency Injection Wiring — Infrastructure Layer232233All the abstract interfaces are wired to concrete implementations in the infrastructure layer (or a DI container). Nothing else in the codebase knows which concrete class is used:234235```python236# infrastructure/container.py237from functools import lru_cache238import asyncpg239from adapters.repositories.postgres_user_repository import PostgresUserRepository240from adapters.gateways.stripe_payment_gateway import StripePaymentAdapter241from use_cases.create_user import CreateUserUseCase242from infrastructure.config import Settings243244@lru_cache245def get_settings() -> Settings:246return Settings()247248async def get_db_pool() -> asyncpg.Pool:249settings = get_settings()250return await asyncpg.create_pool(settings.database_url)251252async def get_create_user_use_case() -> CreateUserUseCase:253pool = await get_db_pool()254repo = PostgresUserRepository(pool=pool)255return CreateUserUseCase(user_repository=repo)256257# In tests, replace get_create_user_use_case with a version258# that injects InMemoryUserRepository — no other code changes needed.259```260261---262263## Aggregate Design Heuristics264265Use these rules when deciding aggregate boundaries:266267| Question | Guidance |268|---|---|269| Should these two objects always be consistent together? | Put them in the same aggregate. |270| Can they be eventually consistent? | Put them in separate aggregates; use domain events to sync. |271| Is one object the "owner" that controls access? | That object is the aggregate root. |272| Does removing the root make the child meaningless? | Child belongs inside the aggregate. |273| Are you loading thousands of objects to change one? | Aggregate is too large — split it. |274275**Practical example — Order vs. Customer:**276277```python278# Bad: Customer aggregate holds full Order objects279class Customer:280def __init__(self):281self._orders: list[Order] = [] # loads all orders every time282283# Good: Customer holds Order IDs only; Order is its own aggregate284class Customer:285def __init__(self):286self._order_ids: list[str] = [] # lightweight reference287288class Order:289def __init__(self, id: str, customer_id: str):290self.id = id291self.customer_id = customer_id # reference back, not the full object292```293294---295296## Domain Events — Publishing and Handling297298Domain events decouple aggregates that need to react to each other's state changes:299300```python301# domain/events/order_events.py302from dataclasses import dataclass, field303from datetime import datetime304305@dataclass306class DomainEvent:307occurred_at: datetime = field(default_factory=datetime.utcnow)308309@dataclass310class OrderSubmittedEvent(DomainEvent):311order_id: str = ""312customer_id: str = ""313total_cents: int = 0314currency: str = "USD"315316317# adapters/event_publisher/postgres_outbox.py318# Transactional outbox pattern: write events to the same DB transaction as state319import json320321class PostgresOutboxPublisher:322"""323Writes domain events to an outbox table in the same transaction324as the aggregate state. A separate relay process reads and publishes325to the message broker. Guarantees at-least-once delivery.326"""327328async def publish(self, conn, events: list[DomainEvent]):329for event in events:330await conn.execute(331"""332INSERT INTO outbox (event_type, payload, published_at)333VALUES ($1, $2, NULL)334""",335type(event).__name__,336json.dumps(event.__dict__, default=str),337)338339340# use_cases/place_order.py — aggregate saves, events are extracted and stored341class PlaceOrderUseCase:342def __init__(self, order_repo: OrderRepository, event_publisher: PostgresOutboxPublisher):343self.orders = order_repo344self.publisher = event_publisher345346async def execute(self, request: PlaceOrderRequest) -> PlaceOrderResponse:347order = Order(id=str(uuid.uuid4()), customer_id=request.customer_id)348for item in request.items:349order.add_item(product=item.product, quantity=item.quantity)350order.submit()351352async with self.db.transaction() as conn:353await self.orders.save(order, conn)354await self.publisher.publish(conn, order.pop_events())355356return PlaceOrderResponse(order_id=order.id, success=True)357```358359---360361## Detecting and Breaking Dependency Cycles362363Common symptoms and their structural fixes:364365```366Symptom: use_cases/create_order.py imports from adapters/email_sender.py367Fix: Create domain/interfaces/notification_service.py (abstract port).368use_cases imports the port. adapters implements it.369DI container wires them together.370371Symptom: domain/entities/user.py imports from infrastructure/config.py372Fix: Pass config values as constructor arguments or environment at373the infrastructure boundary. Domain entities must not read config.374375Symptom: Two aggregates import each other376Fix: Introduce a domain event. Aggregate A emits OrderPlaced.377Aggregate B's use case subscribes and reacts. They never import378each other.379380Symptom: Repository imports a use case to "do extra work" after saving381Fix: Extract the extra work into a separate domain service or use case.382Repositories persist state only; they do not orchestrate behaviour.383```384385Visual dependency check — run this and look for any arrow pointing outward:386387```bash388# Install: pip install pydeps389pydeps app --max-bacon=4 --cluster --rankdir=BT390# Expected: domain has no outgoing edges to adapters or infrastructure391```392