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.
Scanned 9/20/2026
Install to Claude Code
npx -y skills add tstapler/dotfiles --skill code-architecture-best-practices --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Code Architecture Best Practices?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/tstapler-code-architecture-best-practices)More formats (shields.io, HTML) on the badges page.
---
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.
## Reuse Check (Do This First)
Before writing new logic, search the codebase (`grep`/`ast-grep`) for an existing function or
pattern that already does something similar — reuse it or extract a shared abstraction rather
than reimplementing it per call site. This is the single highest-value check in this skill: the
canonical failure mode it catches is the same bug-prone logic (e.g. a check-then-mutate ordering
rule) getting copy-pasted across sibling functions, each with its own chance to get it wrong,
instead of being centralized once. Do this check *before* design-pattern selection below, not
after — a pattern applied to duplicated logic just formalizes the duplication.
If the file you're about to edit is a known churn/complexity hotspot, run `code-hotspot-analysis`'s
inline spot-check first — a file that already changes often and is already complex is exactly
where uncaught duplication compounds fastest.
## Extending Bad Architecture: Refactor, Isolate, or Extend
When the touched area already violates SOLID/Clean/DDD boundaries, don't bolt new code onto the
existing mess by default. Decide explicitly, in this priority order:
1. **Refactor-first** — fix the violation as part of this change, if the fix is small enough to
fit the task budget and the new work would otherwise deepen it (e.g. adding a 6th responsibility
to a class that already has 5).
2. **Isolate via seam** — wrap the legacy area behind an adapter/facade/anti-corruption layer so
the new code gets clean boundaries even though the legacy code inside stays messy. Use this
when a full refactor is out of scope for the change.
3. **Extend as-is** — only when the touched area is stable and this change does not add another
instance of the *same* violation the area already has. A one-line change to a large-but-stable
file is fine; another method on an existing God Object is not.
Never pick "extend as-is" silently — state which of the three you chose and why. Compounding an
existing violation (same kind, one more instance) is a review-blocking finding, not a style nit.
## Core Principles (Language-Agnostic)
### SOLID
- **S**ingle Responsibility: One reason to change per class/module
- **O**pen/Closed: Open for extension, closed for modification (use interfaces/protocols)
- **L**iskov Substitution: Subtypes must be substitutable for base types
- **I**nterface Segregation: Many focused interfaces > one fat interface
- **D**ependency 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
**Make the layer/component rules mechanical, not just reviewed.** If `kibitzer` is available
(`kibitzer --help` on `PATH`, or the `kibitzer` MCP server), it can enforce the Clean/Hexagonal
layer rules and DDD naming conventions above as a batch check instead of relying on an LLM
catching a violation at review time. In `.claude/inspect.json`'s top-level `architecture` section:
- `components` — name the layers/bounded contexts by path glob (e.g. `domain`, `handlers`, `infra`).
- `dependency_rules` — `{component, may_depend_on: [...]}`, deny-by-default: this is the
"dependencies point inward only" rule, enforced. Checker name `component-deps`.
- `content_rules` — `{component, allowed_kinds: [...]}` restricts what a component may declare
(e.g. `domain` only allows `struct`/`class` — keeps framework types from leaking into the core).
Checker name `content-rules`.
- `naming_rules` — `{component, kind, pattern}` (regex) enforces port/adapter naming (e.g. every
`struct` in `infra` must match `^.*(Repository|Client)$`). Checker name `naming-rules`.
Run `kibitzer run <dir> --trigger batch` (CLI) or the `architecture_assessment`/`run_checks` MCP
tools to check current compliance before proposing new components, and add these three checkers
to `.claude/inspect.json` once components are named — this turns a plan-time judgment call into
something CI/PostToolUse catches on every future change. All three cover Go, TypeScript/JavaScript,
Java, Kotlin, and Python. If kibitzer isn't installed, this is a nice-to-have, not a blocker —
fall back to manual review of the same rules.
### 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
```python
from abc import ABC, abstractmethod
from typing import Protocol
# Domain defines the interface (port)
class UserRepository(Protocol):
def find_by_id(self, user_id: str) -> User | None: ...
def save(self, user: User) -> None: ...
# Infrastructure implements it (adapter)
class PostgresUserRepository:
def find_by_id(self, user_id: str) -> User | None:
...
def save(self, user: User) -> None:
...
```
### Dependency Injection
```python
# Application service receives dependencies — never imports them
class OrderService:
def __init__(self, orders: OrderRepository, payments: PaymentService) -> None:
self._orders = orders
self._payments = payments
```
### Value Objects
```python
from dataclasses import dataclass
@dataclass(frozen=True) # frozen=True makes it immutable
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
```java
// Application Service — owns transaction boundary
@Service
@Transactional
public class OrderApplicationService {
private final OrderRepository orderRepository; // interface, not JPA impl
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();
}
}
// Repository interface in domain layer
public interface OrderRepository {
Order save(Order order);
Optional<Order> findById(OrderId id);
}
// JPA implementation in infrastructure layer
@Repository
class JpaOrderRepository implements OrderRepository {
private final OrderJpaRepository jpa; // Spring Data 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:
```python
# Python
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
# Simple in-process event bus
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)
```
```java
// Java/Spring Boot: use ApplicationEventPublisher
@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 |
```python
# Protocol: structural subtyping — no inheritance needed
class BookStorage(Protocol):
def find_by_id(self, book_id: str) -> Book | None: ...
def save(self, book: Book) -> None: ...
# ABC: nominal subtyping + shared implementation
class MergeStrategy(ABC):
@abstractmethod
def can_handle(self, base, local, remote) -> bool: ...
@abstractmethod
def apply(self, base, local, remote) -> list[str]: ...
# Shared logic subclasses inherit
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 |
| `code-hotspot-analysis` | Check whether a file is already a churn/complexity hotspot before a non-trivial edit |
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!