| name | code-architecture-best-practices |
| description | Apply software architecture best practices when designing or reviewing systems, classes, modules, or services. Use when structuring new code, evaluating design decisions, applying SOLID principles, Clean Architecture, Hexagonal Architecture, or Domain-Driven Design patterns. Works across languages โ includes specific guidance for Python and Java/Spring Boot. |
| effort | high |
Architecture Best Practices
Apply these principles when designing systems, reviewing structure, or making architectural decisions.
Core Principles (Language-Agnostic)
SOLID
- Single Responsibility: One reason to change per class/module
- Open/Closed: Open for extension, closed for modification (use interfaces/protocols)
- Liskov Substitution: Subtypes must be substitutable for base types
- Interface Segregation: Many focused interfaces > one fat interface
- Dependency Inversion: Depend on abstractions, not concretions
Clean Architecture Layers
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Frameworks & Drivers (Web, DB) โ โ outermost, most volatile
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Interface Adapters (Controllersโ
โ Presenters, Gateways) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Application Use Cases โ โ orchestrates domain
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Domain Entities & Rules โ โ innermost, most stable
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Rule: Dependencies point inward only. Domain knows nothing about frameworks.
Hexagonal Architecture (Ports & Adapters)
- Ports: Interfaces defined by the domain (what the app needs)
- Adapters: Implementations of ports (HTTP, DB, messaging)
- Core: Business logic with no framework dependencies
- Enables swapping infrastructure without touching domain logic
Domain-Driven Design Essentials
| Concept | Purpose | Rule |
|---|
| Entity | Has identity, mutable | Identified by ID, not attributes |
| Value Object | Immutable, no identity | Equality by value; replace, don't mutate |
| Aggregate | Consistency boundary | Only modify through Aggregate Root |
| Repository | Collection abstraction | One per Aggregate Root; hides persistence |
| Domain Service | Logic not owned by entity | Stateless; operates on multiple entities |
| Application Service | Use case orchestration | No business logic; coordinates domain objects |
Key Design Rules
- Tell, Don't Ask: Objects should do things, not expose state to be checked externally
- Law of Demeter: Don't chain more than one
. (avoid a.b().c().d())
- Composition over Inheritance: Favor has-a over is-a
- Command-Query Separation: Methods either change state (command) or return data (query) โ not both
- Ubiquitous Language: Code names must match domain expert vocabulary exactly
Python-Specific Architecture
Layer Structure
src/
โโโ domain/ # Entities, Value Objects, Domain Services, Repository interfaces
โ โโโ models.py # Pydantic/dataclass domain models
โ โโโ services.py # Pure domain logic
โโโ application/ # Use cases, Application Services, DTOs
โ โโโ use_cases.py
โโโ infrastructure/ # Repository implementations, DB, HTTP clients
โ โโโ repositories.py
โ โโโ clients.py
โโโ interfaces/ # CLI (Typer), API (FastAPI), etc.
โโโ cli.py
Repository Pattern
from abc import ABC, abstractmethod
from typing import Protocol
class UserRepository(Protocol):
def find_by_id(self, user_id: str) -> User | None: ...
def save(self, user: User) -> None: ...
class PostgresUserRepository:
def find_by_id(self, user_id: str) -> User | None:
...
def save(self, user: User) -> None:
...
Dependency Injection
class OrderService:
def __init__(self, orders: OrderRepository, payments: PaymentService) -> None:
self._orders = orders
self._payments = payments
Value Objects
from dataclasses import dataclass
@dataclass(frozen=True)
class Money:
amount: Decimal
currency: str
def add(self, other: "Money") -> "Money":
if self.currency != other.currency:
raise ValueError("Currency mismatch")
return Money(self.amount + other.amount, self.currency)
Avoid
- โ Business logic in CLI/API handlers
- โ Importing ORM models into domain layer
- โ God classes that do everything
- โ Mutable global state
Java / Spring Boot-Specific Architecture
Package Structure (by feature, not layer)
com.example.
โโโ order/
โ โโโ domain/ # Order, OrderItem (entities/VOs)
โ โโโ application/ # OrderService, CreateOrderCommand
โ โโโ infrastructure/ # OrderJpaRepository, OrderMapper
โ โโโ api/ # OrderController, OrderRequest/Response DTOs
โโโ payment/
โ โโโ ...
โโโ shared/ # Shared kernel: common Value Objects, exceptions
Spring Layering Rules
| Layer | Annotation | Responsibility |
|---|
| API | @RestController | HTTP in/out, request validation, DTO mapping |
| Application | @Service | Use case orchestration, transaction boundary |
| Domain | (no Spring) | Pure business logic, entities, rules |
| Infrastructure | @Repository / @Component | DB, external HTTP, messaging |
Dependency Direction
Controller โ ApplicationService โ DomainService/Entity
โ
Repository (interface)
โ
RepositoryImpl (infrastructure)
- Controllers depend on Application Services
- Application Services depend on Repository interfaces (domain layer)
- Infrastructure implements those interfaces
- Domain layer has zero Spring dependencies
Key Spring Boot Patterns
@Service
@Transactional
public class OrderApplicationService {
private final OrderRepository orderRepository;
private final PaymentService paymentService;
public OrderId createOrder(CreateOrderCommand cmd) {
var order = Order.create(cmd.customerId(), cmd.items());
paymentService.reserve(order.total());
return orderRepository.save(order).id();
}
}
public interface OrderRepository {
Order save(Order order);
Optional<Order> findById(OrderId id);
}
@Repository
class JpaOrderRepository implements OrderRepository {
private final OrderJpaRepository jpa;
...
}
Spring Boot Avoid
- โ Business logic in
@RestController
- โ
@Autowired field injection (use constructor injection)
- โ Exposing JPA entities directly in API responses
- โ
@Transactional on domain objects
- โ Cross-feature direct class dependencies (use interfaces or events)
Domain Events
Use events to decouple aggregates and trigger side effects without coupling:
from dataclasses import dataclass, field
from datetime import datetime, UTC
@dataclass(frozen=True)
class DomainEvent:
occurred_at: datetime = field(default_factory=lambda: datetime.now(UTC))
@dataclass(frozen=True)
class OrderPlaced(DomainEvent):
order_id: str
customer_id: str
total: float
class EventBus:
def __init__(self) -> None:
self._handlers: dict[type, list] = {}
def subscribe(self, event_type: type, handler) -> None:
self._handlers.setdefault(event_type, []).append(handler)
def publish(self, event: DomainEvent) -> None:
for handler in self._handlers.get(type(event), []):
handler(event)
@Service
public class OrderService {
private final ApplicationEventPublisher eventPublisher;
public void placeOrder(PlaceOrderCommand cmd) {
Order order = Order.create(cmd);
orderRepository.save(order);
eventPublisher.publishEvent(new OrderPlacedEvent(order.getId()));
}
}
@EventListener
public void onOrderPlaced(OrderPlacedEvent event) {
notificationService.sendConfirmation(event.orderId());
}
Protocol vs ABC (Python)
| Use Protocol | Use ABC |
|---|
| External/3rd-party implementations | Need to enforce explicit inheritance |
| Duck-typing flexibility | Want to share default behavior |
| Testing (easier to mock) | Framework extension points |
| Ports/interfaces in hexagonal arch | Strategy hierarchies with shared logic |
class BookStorage(Protocol):
def find_by_id(self, book_id: str) -> Book | None: ...
def save(self, book: Book) -> None: ...
class MergeStrategy(ABC):
@abstractmethod
def can_handle(self, base, local, remote) -> bool: ...
@abstractmethod
def apply(self, base, local, remote) -> list[str]: ...
def is_safe_merge(self, base, result) -> bool:
return len(result) >= len(base) * 0.5
Cross-Cutting Concerns
Error Handling Strategy
- Domain errors: typed exceptions or Result types (not generic RuntimeException)
- Application layer: translates domain errors to user-facing messages
- Infrastructure layer: wraps external errors, never leaks them to domain
Testing Boundaries
| Layer | Test Type | Strategy |
|---|
| Domain | Unit | Pure functions, no mocks needed |
| Application | Unit | Mock repositories/services |
| Infrastructure | Integration | Real DB (Testcontainers / pytest-docker) |
| API | Integration | Full stack or MockMvc/TestClient |
Configuration
- Externalize all config (no hardcoded URLs, credentials, env-specific values)
- Domain layer never reads config โ inject values via constructors
- Use typed config objects, not raw string lookups scattered through code
Type-Driven Design
Apply techniques from the type-driven-design skill alongside structural patterns. The two work together: patterns describe how components relate; type-driven design describes how to make each component's invariants compiler-enforced.
Key integration points:
- Value Object (PoEAA) โ implement as a smart constructor type:
Money, Email, DateRange
- Repository โ use phantom/newtype IDs:
Repository[User, UserID] prevents cross-entity mixups
- Domain Model โ replace primitive fields with proven types:
status: OrderStatus (sum type), not status: string
- Service Layer boundary โ parse raw input into domain types at the entry point; pass proven types down
Signs the architecture needs type-driven improvements: validation logic repeated across service methods, null/None checks deep inside domain logic, runtime panics from invalid state combinations, string or int parameters that must satisfy undocumented constraints.
Decision Guide
| Situation | Pattern |
|---|
| Multiple implementations of same concept | Repository / Strategy pattern |
| Complex object creation | Factory / Builder |
| Cross-cutting concerns (logging, auth) | Decorator / Middleware |
| Notify other parts of system about events | Domain Events |
| Simplify complex subsystem | Facade (named service) |
| Decouple caller from implementation | Dependency Injection |
| Primitive used where domain type needed | Type-Driven Design (newtype / value object) |
| Invalid states reachable at runtime | Type-Driven Design (sum types / smart constructors) |
| Validation repeated across functions | Type-Driven Design (parse at boundary) |
Related Skills
| Skill | When to apply |
|---|
type-driven-design | Make domain invariants compiler-enforced via newtypes and sum types |
design-patterns | Apply GoF/PoEAA patterns within the architectural layer structure |
python-development | Python-specific standards: uv, pytest, Pydantic, async, hexagonal layout |
code-spring-boot | Spring Boot-specific layering, testing, and dependency injection patterns |
code-refactoring | Restructure existing code toward clean architecture boundaries |
code-review | Verify architectural decisions meet these principles before merging |