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.
SKILL.md
1---2name: architecture-patterns3description: Implement proven backend architecture patterns including Clean Architecture, Hexagonal Architecture, and Domain-Driven Design. Use this skill when designing clean architecture for a new microservice, when refactoring a monolith to use bounded contexts, when implementing hexagonal or onion architecture patterns, or when debugging dependency cycles between application layers.4---56# Architecture Patterns78Master proven backend architecture patterns including Clean Architecture, Hexagonal Architecture, and Domain-Driven Design to build maintainable, testable, and scalable systems.910**Given:** a service boundary or module to architect.11**Produces:** layered structure with clear dependency rules, interface definitions, and test boundaries.1213## When to Use This Skill1415- Designing new backend services or microservices from scratch16- Refactoring monolithic applications where business logic is entangled with ORM models or HTTP concerns17- Establishing bounded contexts before splitting a system into services18- Debugging dependency cycles where infrastructure code bleeds into the domain layer19- Creating testable codebases where use-case tests do not require a running database20- Implementing domain-driven design tactical patterns (aggregates, value objects, domain events)2122## Core Concepts2324### 1. Clean Architecture (Uncle Bob)2526**Layers (dependency flows inward):**2728- **Entities**: Core business models, no framework imports29- **Use Cases**: Application business rules, orchestrate entities30- **Interface Adapters**: Controllers, presenters, gateways — translate between use cases and external formats31- **Frameworks & Drivers**: UI, database, external services — all at the outermost ring3233**Key Principles:**3435- Dependencies point inward only; inner layers know nothing about outer layers36- Business logic is independent of frameworks, databases, and delivery mechanisms37- Every layer boundary is crossed via an abstract interface38- Testable without UI, database, or external services3940### 2. Hexagonal Architecture (Ports and Adapters)4142**Components:**4344- **Domain Core**: Business logic lives here, framework-free45- **Ports**: Abstract interfaces that define how the core interacts with the outside world (driving and driven)46- **Adapters**: Concrete implementations of ports (PostgreSQL adapter, Stripe adapter, REST adapter)4748**Benefits:**4950- Swap implementations without touching the core (e.g., replace PostgreSQL with DynamoDB)51- Use in-memory adapters in tests — no Docker required52- Technology decisions deferred to the edges5354### 3. Domain-Driven Design (DDD)5556**Strategic Patterns:**5758- **Bounded Contexts**: Isolate a coherent model for one subdomain; avoid sharing a single model across the whole system59- **Context Mapping**: Define how contexts relate (Anti-Corruption Layer, Shared Kernel, Open Host Service)60- **Ubiquitous Language**: Every term in code matches the term used by domain experts6162**Tactical Patterns:**6364- **Entities**: Objects with stable identity that change over time65- **Value Objects**: Immutable objects identified by their attributes (Email, Money, Address)66- **Aggregates**: Consistency boundaries; only the root is accessible from outside67- **Repositories**: Persist and reconstitute aggregates; abstract over the storage mechanism68- **Domain Events**: Capture things that happened inside the domain; used for cross-aggregate coordination6970## Clean Architecture — Directory Structure7172```73app/74├── domain/ # Entities, value objects, interfaces75│ ├── entities/76│ │ ├── user.py77│ │ └── order.py78│ ├── value_objects/79│ │ ├── email.py80│ │ └── money.py81│ └── interfaces/ # Abstract ports (no implementations)82│ ├── user_repository.py83│ └── payment_gateway.py84├── use_cases/ # Application business rules85│ ├── create_user.py86│ ├── process_order.py87│ └── send_notification.py88├── adapters/ # Concrete implementations89│ ├── repositories/90│ │ ├── postgres_user_repository.py91│ │ └── redis_cache_repository.py92│ ├── controllers/93│ │ └── user_controller.py94│ └── gateways/95│ ├── stripe_payment_gateway.py96│ └── sendgrid_email_gateway.py97└── infrastructure/ # Framework wiring, config, DI container98├── database.py99├── config.py100└── logging.py101```102103**Dependency rule in one sentence:** every `import` statement in `domain/` and `use_cases/` must point only toward `domain/`; nothing in those layers may import from `adapters/` or `infrastructure/`.104105## Clean Architecture — Core Implementation106107```python108# domain/entities/user.py109from dataclasses import dataclass110from datetime import datetime111112@dataclass113class User:114"""Core user entity — no framework dependencies."""115id: str116email: str117name: str118created_at: datetime119is_active: bool = True120121def deactivate(self):122self.is_active = False123124def can_place_order(self) -> bool:125return self.is_active126127128# domain/interfaces/user_repository.py129from abc import ABC, abstractmethod130from typing import Optional131from domain.entities.user import User132133class IUserRepository(ABC):134"""Port: defines contract, no implementation details."""135136@abstractmethod137async def find_by_id(self, user_id: str) -> Optional[User]: ...138139@abstractmethod140async def find_by_email(self, email: str) -> Optional[User]: ...141142@abstractmethod143async def save(self, user: User) -> User: ...144145@abstractmethod146async def delete(self, user_id: str) -> bool: ...147148149# use_cases/create_user.py150from dataclasses import dataclass151from datetime import datetime152from typing import Optional153import uuid154from domain.entities.user import User155from domain.interfaces.user_repository import IUserRepository156157@dataclass158class CreateUserRequest:159email: str160name: str161162@dataclass163class CreateUserResponse:164user: Optional[User]165success: bool166error: Optional[str] = None167168class CreateUserUseCase:169"""Use case: orchestrates business logic, no HTTP or DB details."""170171def __init__(self, user_repository: IUserRepository):172self.user_repository = user_repository173174async def execute(self, request: CreateUserRequest) -> CreateUserResponse:175existing = await self.user_repository.find_by_email(request.email)176if existing:177return CreateUserResponse(user=None, success=False, error="Email already exists")178179user = User(180id=str(uuid.uuid4()),181email=request.email,182name=request.name,183created_at=datetime.now(),184)185saved_user = await self.user_repository.save(user)186return CreateUserResponse(user=saved_user, success=True)187188189# adapters/repositories/postgres_user_repository.py190from domain.interfaces.user_repository import IUserRepository191from domain.entities.user import User192from typing import Optional193import asyncpg194195class PostgresUserRepository(IUserRepository):196"""Adapter: PostgreSQL implementation of the user port."""197198def __init__(self, pool: asyncpg.Pool):199self.pool = pool200201async def find_by_id(self, user_id: str) -> Optional[User]:202async with self.pool.acquire() as conn:203row = await conn.fetchrow("SELECT * FROM users WHERE id = $1", user_id)204return self._to_entity(row) if row else None205206async def find_by_email(self, email: str) -> Optional[User]:207async with self.pool.acquire() as conn:208row = await conn.fetchrow("SELECT * FROM users WHERE email = $1", email)209return self._to_entity(row) if row else None210211async def save(self, user: User) -> User:212async with self.pool.acquire() as conn:213await conn.execute(214"""215INSERT INTO users (id, email, name, created_at, is_active)216VALUES ($1, $2, $3, $4, $5)217ON CONFLICT (id) DO UPDATE218SET email = $2, name = $3, is_active = $5219""",220user.id, user.email, user.name, user.created_at, user.is_active,221)222return user223224async def delete(self, user_id: str) -> bool:225async with self.pool.acquire() as conn:226result = await conn.execute("DELETE FROM users WHERE id = $1", user_id)227return result == "DELETE 1"228229def _to_entity(self, row) -> User:230return User(231id=row["id"], email=row["email"], name=row["name"],232created_at=row["created_at"], is_active=row["is_active"],233)234235236# adapters/controllers/user_controller.py237from fastapi import APIRouter, Depends, HTTPException238from pydantic import BaseModel239from use_cases.create_user import CreateUserUseCase, CreateUserRequest240241router = APIRouter()242243class CreateUserDTO(BaseModel):244email: str245name: str246247@router.post("/users")248async def create_user(249dto: CreateUserDTO,250use_case: CreateUserUseCase = Depends(get_create_user_use_case),251):252"""Controller handles HTTP only — no business logic lives here."""253response = await use_case.execute(CreateUserRequest(email=dto.email, name=dto.name))254if not response.success:255raise HTTPException(status_code=400, detail=response.error)256return {"user": response.user}257```258259## Hexagonal Architecture — Ports and Adapters260261```python262# Core domain service — no infrastructure dependencies263class OrderService:264def __init__(265self,266order_repository: OrderRepositoryPort,267payment_gateway: PaymentGatewayPort,268notification_service: NotificationPort,269):270self.orders = order_repository271self.payments = payment_gateway272self.notifications = notification_service273274async def place_order(self, order: Order) -> OrderResult:275if not order.is_valid():276return OrderResult(success=False, error="Invalid order")277278payment = await self.payments.charge(amount=order.total, customer=order.customer_id)279if not payment.success:280return OrderResult(success=False, error="Payment failed")281282order.mark_as_paid()283saved_order = await self.orders.save(order)284await self.notifications.send(285to=order.customer_email,286subject="Order confirmed",287body=f"Order {order.id} confirmed",288)289return OrderResult(success=True, order=saved_order)290291292# Ports (driving and driven interfaces)293class OrderRepositoryPort(ABC):294@abstractmethod295async def save(self, order: Order) -> Order: ...296297class PaymentGatewayPort(ABC):298@abstractmethod299async def charge(self, amount: Money, customer: str) -> PaymentResult: ...300301class NotificationPort(ABC):302@abstractmethod303async def send(self, to: str, subject: str, body: str): ...304305306# Production adapter: Stripe307class StripePaymentAdapter(PaymentGatewayPort):308def __init__(self, api_key: str):309import stripe310stripe.api_key = api_key311self._stripe = stripe312313async def charge(self, amount: Money, customer: str) -> PaymentResult:314try:315charge = self._stripe.Charge.create(316amount=amount.cents, currency=amount.currency, customer=customer317)318return PaymentResult(success=True, transaction_id=charge.id)319except self._stripe.error.CardError as e:320return PaymentResult(success=False, error=str(e))321322323# Test adapter: no external dependencies324class MockPaymentAdapter(PaymentGatewayPort):325async def charge(self, amount: Money, customer: str) -> PaymentResult:326return PaymentResult(success=True, transaction_id="mock-txn-123")327```328329## DDD — Value Objects and Aggregates330331```python332# Value Objects: immutable, validated at construction333from dataclasses import dataclass334335@dataclass(frozen=True)336class Email:337value: str338339def __post_init__(self):340if "@" not in self.value or "." not in self.value.split("@")[-1]:341raise ValueError(f"Invalid email: {self.value}")342343@dataclass(frozen=True)344class Money:345amount: int # cents346currency: str347348def __post_init__(self):349if self.amount < 0:350raise ValueError("Money amount cannot be negative")351if self.currency not in {"USD", "EUR", "GBP"}:352raise ValueError(f"Unsupported currency: {self.currency}")353354def add(self, other: "Money") -> "Money":355if self.currency != other.currency:356raise ValueError("Currency mismatch")357return Money(self.amount + other.amount, self.currency)358359360# Aggregate root: enforces all invariants for its cluster of entities361class Order:362def __init__(self, id: str, customer_id: str):363self.id = id364self.customer_id = customer_id365self.items: list[OrderItem] = []366self.status = OrderStatus.PENDING367self._events: list[DomainEvent] = []368369def add_item(self, product: Product, quantity: int):370if self.status != OrderStatus.PENDING:371raise ValueError("Cannot modify a submitted order")372item = OrderItem(product=product, quantity=quantity)373self.items.append(item)374self._events.append(ItemAddedEvent(order_id=self.id, item=item))375376@property377def total(self) -> Money:378totals = [item.subtotal() for item in self.items]379return sum(totals[1:], totals[0]) if totals else Money(0, "USD")380381def submit(self):382if not self.items:383raise ValueError("Cannot submit an empty order")384if self.status != OrderStatus.PENDING:385raise ValueError("Order already submitted")386self.status = OrderStatus.SUBMITTED387self._events.append(OrderSubmittedEvent(order_id=self.id))388389def pop_events(self) -> list[DomainEvent]:390events, self._events = self._events, []391return events392393394# Repository: persist and reconstitute aggregates395class OrderRepository(ABC):396@abstractmethod397async def find_by_id(self, order_id: str) -> Optional[Order]: ...398399@abstractmethod400async def save(self, order: Order) -> None: ...401# Implementations persist events via pop_events() after writing state402```403404## Testing — In-Memory Adapters405406The hallmark of correctly applied Clean Architecture is that every use case can be exercised in a plain unit test with no real database, no Docker, and no network:407408```python409# tests/unit/test_create_user.py410import asyncio411from typing import Dict, Optional412from domain.entities.user import User413from domain.interfaces.user_repository import IUserRepository414from use_cases.create_user import CreateUserUseCase, CreateUserRequest415416417class InMemoryUserRepository(IUserRepository):418def __init__(self):419self._store: Dict[str, User] = {}420421async def find_by_id(self, user_id: str) -> Optional[User]:422return self._store.get(user_id)423424async def find_by_email(self, email: str) -> Optional[User]:425return next((u for u in self._store.values() if u.email == email), None)426427async def save(self, user: User) -> User:428self._store[user.id] = user429return user430431async def delete(self, user_id: str) -> bool:432return self._store.pop(user_id, None) is not None433434435async def test_create_user_succeeds():436repo = InMemoryUserRepository()437use_case = CreateUserUseCase(user_repository=repo)438439response = await use_case.execute(CreateUserRequest(email="[email protected]", name="Alice"))440441assert response.success442assert response.user.email == "[email protected]"443assert response.user.id is not None444445446async def test_duplicate_email_rejected():447repo = InMemoryUserRepository()448use_case = CreateUserUseCase(user_repository=repo)449450await use_case.execute(CreateUserRequest(email="[email protected]", name="Alice"))451response = await use_case.execute(CreateUserRequest(email="[email protected]", name="Alice2"))452453assert not response.success454assert "already exists" in response.error455```456457## Troubleshooting458459### Use case tests require a running database460461Business logic has leaked into the infrastructure layer. Move all database calls behind an `IRepository` interface and inject an in-memory implementation in tests (see Testing section above). The use case constructor must accept the abstract port, not the concrete class.462463### Circular imports between layers464465A common symptom is `ImportError: cannot import name X` between `use_cases` and `adapters`. This happens when a use case imports a concrete adapter class instead of the abstract port. Enforce the rule: `use_cases/` imports only from `domain/` (entities and interfaces). It must never import from `adapters/` or `infrastructure/`.466467### Framework decorators appearing in domain entities468469If SQLAlchemy `Column()` or Pydantic `Field()` annotations appear on domain entities, the entity is no longer pure. Create a separate ORM model in `adapters/repositories/` and map to/from the domain entity in the repository's `_to_entity()` method.470471### All logic ending up in controllers472473When the controller grows beyond HTTP parsing and response formatting, extract the logic into a use case class. A controller method should do three things only: parse the request, call a use case, map the response.474475### Value objects raising errors too late476477Validate invariants in `__post_init__` (Python) or the constructor so an invalid `Email` or `Money` cannot be constructed at all. This surfaces bad data at the boundary, not deep inside business logic.478479### Context bleed across bounded contexts480481If the `Order` context is importing `User` entities from the `Identity` context, introduce an Anti-Corruption Layer. The `Order` context should hold its own lightweight `CustomerId` value object and only call the `Identity` context through an explicit interface.482483## Advanced Patterns484485For detailed DDD bounded context mapping, full multi-service project trees, Anti-Corruption Layer implementations, and Onion Architecture comparisons, see:486487- [`references/advanced-patterns.md`](references/advanced-patterns.md)488489## Related Skills490491- `microservices-patterns` — Apply these architecture patterns when decomposing a monolith into services492- `cqrs-implementation` — Use Clean Architecture as the structural foundation for CQRS command/query separation493- `saga-orchestration` — Sagas require well-defined aggregate boundaries, which DDD tactical patterns provide494- `event-store-design` — Domain events produced by aggregates feed directly into an event store495