Pro Software Architecture
Streamline your workflow with this guide, quality, focused, software. Includes structured workflows, validation checks, and reusable patterns for development.
Pro Software Architecture
A foundational skill for software architecture based on Clean Architecture and Domain-Driven Design principles. Covers layered architecture, dependency inversion, domain modeling, bounded contexts, and quality-focused development practices.
When to Use This Skill
Choose this skill when:
- Designing application architecture with clear separation of concerns
- Implementing Clean Architecture layers (domain, application, infrastructure)
- Modeling business domains with DDD patterns (entities, value objects, aggregates)
- Establishing dependency rules between architectural layers
- Refactoring monolithic code into well-structured, maintainable modules
Consider alternatives when:
- Making system-level design decisions ā use a senior architect skill
- Working on microservice boundaries ā use a microservices skill
- Setting up infrastructure ā use a DevOps or cloud skill
- Need specific framework patterns ā use that framework's skill
Quick Start
# Clean Architecture Layer Structure
src/
āāā domain/ # Core business logic (no dependencies)
ā āāā entities/ # Business objects with identity
ā āāā values/ # Immutable value objects
ā āāā events/ # Domain events
ā āāā ports/ # Interfaces (repository, service contracts)
āāā application/ # Use cases (depends only on domain)
ā āāā commands/ # Write operations
ā āāā queries/ # Read operations
ā āāā services/ # Application services / orchestration
āāā infrastructure/ # External concerns (depends on domain + application)
ā āāā persistence/ # Database implementations
ā āāā messaging/ # Event bus, queues
ā āāā external/ # Third-party API clients
āāā presentation/ # UI or API layer (depends on application)
āāā api/ # REST/GraphQL controllers
āāā web/ # Frontend rendering
Core Concepts
Dependency Rule
| Layer | Can Depend On | Cannot Depend On |
|---|---|---|
| Domain | Nothing (pure business logic) | Application, Infrastructure, Presentation |
| Application | Domain | Infrastructure, Presentation |
| Infrastructure | Domain, Application | Presentation |
| Presentation | Application | Domain directly, Infrastructure |
Domain Model Patterns
// Entity ā has identity, mutable state class Order { private constructor( public readonly id: OrderId, private items: OrderItem[], private status: OrderStatus, ) {} static create(customerId: CustomerId): Order { return new Order(OrderId.generate(), [], OrderStatus.Draft); } addItem(product: ProductId, quantity: number, price: Money): void { if (this.status !== OrderStatus.Draft) { throw new DomainError('Cannot modify a submitted order'); } this.items.push(new OrderItem(product, quantity, price)); } submit(): DomainEvent[] { if (this.items.length === 0) { throw new DomainError('Cannot submit an empty order'); } this.status = OrderStatus.Submitted; return [new OrderSubmitted(this.id, this.total())]; } total(): Money { return this.items.reduce((sum, item) => sum.add(item.subtotal()), Money.zero()); } } // Value Object ā immutable, equality by value class Money { constructor( public readonly amount: number, public readonly currency: string, ) { if (amount < 0) throw new DomainError('Money cannot be negative'); } add(other: Money): Money { if (this.currency !== other.currency) throw new DomainError('Currency mismatch'); return new Money(this.amount + other.amount, this.currency); } equals(other: Money): boolean { return this.amount === other.amount && this.currency === other.currency; } static zero(currency = 'USD'): Money { return new Money(0, currency); } } // Port ā interface defined in domain, implemented in infrastructure interface OrderRepository { findById(id: OrderId): Promise<Order | null>; save(order: Order): Promise<void>; nextId(): OrderId; }
Use Case Pattern
// Application layer ā orchestrates domain objects class SubmitOrderUseCase { constructor( private orders: OrderRepository, private eventBus: EventBus, ) {} async execute(command: SubmitOrderCommand): Promise<void> { const order = await this.orders.findById(command.orderId); if (!order) throw new NotFoundError('Order', command.orderId); const events = order.submit(); await this.orders.save(order); for (const event of events) { await this.eventBus.publish(event); } } }
Configuration
| Parameter | Type | Default | Description |
|---|---|---|---|
architectureStyle | string | 'clean' | Style: clean, hexagonal, or onion |
domainModeling | string | 'ddd' | Domain patterns: DDD, anemic, or active record |
dependencyInjection | string | 'constructor' | DI strategy: constructor, interface, or container |
eventPattern | string | 'domain-events' | Events: domain-events, event-sourcing, or none |
testStrategy | string | 'ports-and-adapters' | Testing: mock ports, in-memory adapters |
moduleStructure | string | 'by-feature' | Organization: by-feature or by-layer |
Best Practices
-
The domain layer has zero external dependencies ā No framework imports, no database drivers, no HTTP libraries. Domain logic is pure business rules expressed in the language of the domain. This makes it testable without mocking infrastructure.
-
Define interfaces (ports) in the domain, implement in infrastructure ā The repository interface lives in the domain layer. The PostgreSQL implementation lives in infrastructure. The domain declares what it needs; infrastructure provides how.
-
Use value objects for concepts that have no identity ā Money, EmailAddress, DateRange, and Coordinates are value objects: immutable, compared by value, self-validating. They prevent primitive obsession and encapsulate validation rules.
-
Organize code by feature (bounded context), not by layer ā A directory per feature (orders/, customers/, payments/) with subdirectories for layers is more navigable than global directories per layer. Related code lives together.
-
Keep use cases thin: orchestrate, don't implement ā Use cases coordinate domain objects and infrastructure services. Business rules belong in entities and value objects. If a use case contains complex logic, it's missing a domain concept.
Common Issues
Domain layer accumulates infrastructure dependencies ā Over time, developers add database-specific annotations, framework decorators, or HTTP-related types to domain entities. Enforce the dependency rule in code reviews and consider using architecture linting tools like ArchUnit.
Anemic domain models with all logic in services ā Entities that are just data containers with getters and setters push all behavior into service classes. Move behavior to entities: order.submit() instead of orderService.submitOrder(order).
Over-engineering simple CRUD operations with full Clean Architecture ā Not every feature needs entities, value objects, use cases, and ports. Simple CRUD with no business rules can use a thinner architecture. Apply Clean Architecture where domain complexity justifies it.
Reviews
No reviews yet. Be the first to review this template!