Architecture Patterns
Workflow
Follow these steps sequentially when applying architecture patterns to a codebase:
1. Identify domain boundaries and define bounded contexts
- Map the business domain into distinct bounded contexts with clear responsibilities
- Define the ubiquitous language for each context
- Determine context relationships (shared kernel, customer-supplier, anti-corruption layer)
2. Define core entities and value objects
- Model entities (objects with identity and lifecycle) in the domain layer
- Extract value objects (immutable, identity-less) for concepts like Money, Email, Address
- Keep all business rules inside entities and value objects -- no logic in services or controllers
3. Create repository interfaces (ports)
- Define abstract interfaces in the domain layer for all external dependencies
- Ports include: data persistence, external APIs, messaging, notifications
- Domain code depends only on these interfaces, never on concrete implementations
4. Implement adapters
- Write concrete implementations of each port (Postgres repository, Stripe gateway, SQS publisher)
- Adapters live in an outer layer and import the domain -- never the reverse
- Create test doubles (in-memory repositories, mock gateways) implementing the same ports
5. Wire use cases to orchestrate business logic
- Each use case class receives ports via constructor injection
- Use cases coordinate domain objects and ports to fulfill a single application operation
- Return result objects, not raw domain entities, to decouple callers from domain internals
6. Validate architecture constraints
- Dependency rule: all dependencies point inward (infrastructure -> application -> domain)
- Domain purity: domain layer has zero framework imports
- Interface segregation: ports are small and focused on one capability
- Thin controllers: HTTP/CLI handlers only translate input/output and delegate to use cases
Reference Implementation
This trimmed example shows the key relationships: entity -> port -> use case -> adapter.
# domain/entities/user.py — Core entity, no framework dependencies
@dataclass
class User:
id: str
email: str
name: str
is_active: bool = True
def deactivate(self):
self.is_active = False
# domain/interfaces/user_repository.py — Port (abstract interface in domain)
class IUserRepository(ABC):
@abstractmethod
async def find_by_email(self, email: str) -> Optional[User]: ...
@abstractmethod
async def save(self, user: User) -> User: ...
# use_cases/create_user.py — Orchestrates domain + ports
class CreateUserUseCase:
def __init__(self, user_repo: IUserRepository):
self.user_repo = user_repo
async def execute(self, email: str, name: str) -> User:
if await self.user_repo.find_by_email(email):
raise ValueError("Email already exists")
user = User(id=str(uuid4()), email=email, name=name)
return await self.user_repo.save(user)
# adapters/postgres_user_repository.py — Adapter (implements port)
class PostgresUserRepository(IUserRepository):
def __init__(self, pool: asyncpg.Pool):
self.pool = pool
async def find_by_email(self, email: str) -> Optional[User]:
row = await self.pool.fetchrow("SELECT * FROM users WHERE email=$1", email)
return User(**row) if row else None
async def save(self, user: User) -> User:
await self.pool.execute(
"INSERT INTO users (id,email,name,is_active) VALUES ($1,$2,$3,$4) "
"ON CONFLICT (id) DO UPDATE SET email=$2, name=$3, is_active=$4",
user.id, user.email, user.name, user.is_active)
return user
Directory Structure
app/
├── domain/ # Entities, value objects, ports (interfaces)
│ ├── entities/
│ ├── value_objects/
│ └── interfaces/
├── use_cases/ # Application business rules
├── adapters/ # Port implementations (DB, APIs, messaging)
│ ├── repositories/
│ ├── controllers/
│ └── gateways/
└── infrastructure/ # Framework config, logging, DI wiring
Resources
- references/clean-architecture-guide.md: Detailed layer breakdown
- references/hexagonal-architecture-guide.md: Ports and adapters patterns
- references/ddd-tactical-patterns.md: Entities, value objects, aggregates
- assets/clean-architecture-template/: Complete project structure
- assets/ddd-examples/: Domain modeling examples
1---2name: architecture-patterns3description: Apply Clean Architecture, Hexagonal (ports and adapters), onion architecture, layered architecture, and DDD patterns to backend systems. Define bounded contexts, create port/adapter interfaces, organize dependency layers, separate domain from infrastructure, enforce dependency inversion, and structure project layout for separation of concerns. Use when designing new services, refactoring tightly coupled code, planning microservices decomposition, or establishing project structure conventions.4---56# Architecture Patterns78## Workflow910Follow these steps sequentially when applying architecture patterns to a codebase:1112### 1. Identify domain boundaries and define bounded contexts1314- Map the business domain into distinct bounded contexts with clear responsibilities15- Define the ubiquitous language for each context16- Determine context relationships (shared kernel, customer-supplier, anti-corruption layer)1718### 2. Define core entities and value objects1920- Model entities (objects with identity and lifecycle) in the domain layer21- Extract value objects (immutable, identity-less) for concepts like Money, Email, Address22- Keep all business rules inside entities and value objects -- no logic in services or controllers2324### 3. Create repository interfaces (ports)2526- Define abstract interfaces in the domain layer for all external dependencies27- Ports include: data persistence, external APIs, messaging, notifications28- Domain code depends only on these interfaces, never on concrete implementations2930### 4. Implement adapters3132- Write concrete implementations of each port (Postgres repository, Stripe gateway, SQS publisher)33- Adapters live in an outer layer and import the domain -- never the reverse34- Create test doubles (in-memory repositories, mock gateways) implementing the same ports3536### 5. Wire use cases to orchestrate business logic3738- Each use case class receives ports via constructor injection39- Use cases coordinate domain objects and ports to fulfill a single application operation40- Return result objects, not raw domain entities, to decouple callers from domain internals4142### 6. Validate architecture constraints4344- **Dependency rule**: all dependencies point inward (infrastructure -> application -> domain)45- **Domain purity**: domain layer has zero framework imports46- **Interface segregation**: ports are small and focused on one capability47- **Thin controllers**: HTTP/CLI handlers only translate input/output and delegate to use cases4849## Reference Implementation5051This trimmed example shows the key relationships: entity -> port -> use case -> adapter.5253```python54# domain/entities/user.py — Core entity, no framework dependencies55@dataclass56class User:57 id: str58 email: str59 name: str60 is_active: bool = True6162 def deactivate(self):63 self.is_active = False6465# domain/interfaces/user_repository.py — Port (abstract interface in domain)66class IUserRepository(ABC):67 @abstractmethod68 async def find_by_email(self, email: str) -> Optional[User]: ...69 @abstractmethod70 async def save(self, user: User) -> User: ...7172# use_cases/create_user.py — Orchestrates domain + ports73class CreateUserUseCase:74 def __init__(self, user_repo: IUserRepository):75 self.user_repo = user_repo7677 async def execute(self, email: str, name: str) -> User:78 if await self.user_repo.find_by_email(email):79 raise ValueError("Email already exists")80 user = User(id=str(uuid4()), email=email, name=name)81 return await self.user_repo.save(user)8283# adapters/postgres_user_repository.py — Adapter (implements port)84class PostgresUserRepository(IUserRepository):85 def __init__(self, pool: asyncpg.Pool):86 self.pool = pool8788 async def find_by_email(self, email: str) -> Optional[User]:89 row = await self.pool.fetchrow("SELECT * FROM users WHERE email=$1", email)90 return User(**row) if row else None9192 async def save(self, user: User) -> User:93 await self.pool.execute(94 "INSERT INTO users (id,email,name,is_active) VALUES ($1,$2,$3,$4) "95 "ON CONFLICT (id) DO UPDATE SET email=$2, name=$3, is_active=$4",96 user.id, user.email, user.name, user.is_active)97 return user98```99100## Directory Structure101102```103app/104├── domain/ # Entities, value objects, ports (interfaces)105│ ├── entities/106│ ├── value_objects/107│ └── interfaces/108├── use_cases/ # Application business rules109├── adapters/ # Port implementations (DB, APIs, messaging)110│ ├── repositories/111│ ├── controllers/112│ └── gateways/113└── infrastructure/ # Framework config, logging, DI wiring114```115116## Resources117118- **references/clean-architecture-guide.md**: Detailed layer breakdown119- **references/hexagonal-architecture-guide.md**: Ports and adapters patterns120- **references/ddd-tactical-patterns.md**: Entities, value objects, aggregates121- **assets/clean-architecture-template/**: Complete project structure122- **assets/ddd-examples/**: Domain modeling examples