Agent Skills Framework Extension
Backend Architecture Patterns Skill
When to Use This Skill
Use this skill when implementing backend architecture patterns patterns in your codebase.
How to Use This Skill
- Review the patterns and examples below
- Apply the relevant patterns to your implementation
- Follow the best practices outlined in this skill
Service design, data flow, scalability patterns, and backend system architecture.
Core Capabilities
- Clean Architecture - Separation of concerns
- Domain-Driven Design - Business logic organization
- Repository Pattern - Data access abstraction
- Service Layer - Business operation encapsulation
- Dependency Injection - Loose coupling and testability
Clean Architecture Structure
src/
├── domain/ # Core business logic (innermost layer)
│ ├── entities/ # Business entities
│ ├── value-objects/ # Immutable value types
│ ├── events/ # Domain events
│ ├── errors/ # Domain-specific errors
│ └── interfaces/ # Repository interfaces
│
├── application/ # Use cases (application layer)
│ ├── use-cases/ # Application business rules
│ ├── services/ # Application services
│ ├── dtos/ # Data transfer objects
│ └── interfaces/ # Port interfaces
│
├── infrastructure/ # External concerns (outer layer)
│ ├── persistence/ # Database implementations
│ │ ├── repositories/ # Repository implementations
│ │ ├── mappers/ # Entity-to-model mappers
│ │ └── models/ # ORM models
│ ├── messaging/ # Message queue implementations
│ ├── external/ # External service clients
│ └── config/ # Configuration loading
│
└── presentation/ # UI/API layer (outermost)
├── http/ # REST API
│ ├── controllers/ # HTTP request handlers
│ ├── middleware/ # HTTP middleware
│ └── routes/ # Route definitions
├── graphql/ # GraphQL API
└── grpc/ # gRPC services
Domain Layer
// src/domain/entities/order.ts
import { Entity } from './base/entity';
import { OrderId } from '../value-objects/order-id';
import { Money } from '../value-objects/money';
import { OrderItem } from './order-item';
import { OrderStatus } from '../value-objects/order-status';
import { OrderCreatedEvent } from '../events/order-created';
import { OrderConfirmedEvent } from '../events/order-confirmed';
interface OrderProps {
customerId: string;
items: OrderItem[];
status: OrderStatus;
totalAmount: Money;
createdAt: Date;
updatedAt: Date;
}
export class Order extends Entity<OrderId, OrderProps> {
private constructor(id: OrderId, props: OrderProps) {
super(id, props);
}
// Factory method
static create(customerId: string, items: OrderItem[]): Order {
const totalAmount = items.reduce(
(sum, item) => sum.add(item.totalPrice),
Money.zero()
);
const order = new Order(OrderId.generate(), {
customerId,
items,
status: OrderStatus.PENDING,
totalAmount,
createdAt: new Date(),
updatedAt: new Date(),
});
order.addDomainEvent(new OrderCreatedEvent(order));
return order;
}
// Business methods
addItem(item: OrderItem): void {
this.guardAgainstModification();
this.props.items.push(item);
this.props.totalAmount = this.props.totalAmount.add(item.totalPrice);
this.props.updatedAt = new Date();
}
removeItem(itemId: string): void {
this.guardAgainstModification();
const index = this.props.items.findIndex(i => i.id === itemId);
if (index === -1) {
throw new OrderItemNotFoundError(itemId);
}
const removed = this.props.items.splice(index, 1)[0];
this.props.totalAmount = this.props.totalAmount.subtract(removed.totalPrice);
this.props.updatedAt = new Date();
}
confirm(): void {
if (this.props.status !== OrderStatus.PENDING) {
throw new InvalidOrderStateError('Only pending orders can be confirmed');
}
if (this.props.items.length === 0) {
throw new EmptyOrderError('Cannot confirm order with no items');
}
this.props.status = OrderStatus.CONFIRMED;
this.props.updatedAt = new Date();
this.addDomainEvent(new OrderConfirmedEvent(this));
}
cancel(reason: string): void {
if (!this.isCancellable()) {
throw new InvalidOrderStateError('Order cannot be cancelled in current state');
}
this.props.status = OrderStatus.CANCELLED;
this.props.updatedAt = new Date();
this.addDomainEvent(new OrderCancelledEvent(this, reason));
}
// Invariants
private guardAgainstModification(): void {
if (this.props.status !== OrderStatus.PENDING) {
throw new InvalidOrderStateError('Cannot modify non-pending order');
}
}
private isCancellable(): boolean {
return [OrderStatus.PENDING, OrderStatus.CONFIRMED].includes(this.props.status);
}
// Getters (no direct prop access)
get customerId(): string { return this.props.customerId; }
get items(): readonly OrderItem[] { return [...this.props.items]; }
get status(): OrderStatus { return this.props.status; }
get totalAmount(): Money { return this.props.totalAmount; }
}
// src/domain/value-objects/money.ts
export class Money {
private constructor(
private readonly amount: number,
private readonly currency: string
) {
if (amount < 0) throw new InvalidMoneyError('Amount cannot be negative');
}
static of(amount: number, currency: string = 'USD'): Money {
return new Money(amount, currency);
}
static zero(currency: string = 'USD'): Money {
return new Money(0, currency);
}
add(other: Money): Money {
this.ensureSameCurrency(other);
return new Money(this.amount + other.amount, this.currency);
}
subtract(other: Money): Money {
this.ensureSameCurrency(other);
return new Money(this.amount - other.amount, this.currency);
}
multiply(factor: number): Money {
return new Money(this.amount * factor, this.currency);
}
private ensureSameCurrency(other: Money): void {
if (this.currency !== other.currency) {
throw new CurrencyMismatchError(this.currency, other.currency);
}
}
equals(other: Money): boolean {
return this.amount === other.amount && this.currency === other.currency;
}
toJSON(): { amount: number; currency: string } {
return { amount: this.amount, currency: this.currency };
}
}
// src/domain/interfaces/order-repository.ts
export interface OrderRepository {
findById(id: OrderId): Promise<Order | null>;
findByCustomerId(customerId: string, pagination: Pagination): Promise<Order[]>;
save(order: Order): Promise<void>;
delete(id: OrderId): Promise<void>;
}
Application Layer
// src/application/use-cases/create-order.ts
import { UseCase } from './base/use-case';
import { OrderRepository } from '../../domain/interfaces/order-repository';
import { ProductService } from '../services/product-service';
import { EventDispatcher } from '../../domain/events/event-dispatcher';
import { Order } from '../../domain/entities/order';
import { OrderItem } from '../../domain/entities/order-item';
interface CreateOrderInput {
customerId: string;
items: Array<{
productId: string;
quantity: number;
}>;
}
interface CreateOrderOutput {
orderId: string;
totalAmount: number;
status: string;
}
export class CreateOrderUseCase implements UseCase<CreateOrderInput, CreateOrderOutput> {
constructor(
private readonly orderRepository: OrderRepository,
private readonly productService: ProductService,
private readonly eventDispatcher: EventDispatcher
) {}
async execute(input: CreateOrderInput): Promise<CreateOrderOutput> {
// Validate and fetch products
const products = await this.productService.getProducts(
input.items.map(i => i.productId)
);
// Create order items
const orderItems = input.items.map(item => {
const product = products.find(p => p.id === item.productId);
if (!product) {
throw new ProductNotFoundError(item.productId);
}
if (!product.isAvailable(item.quantity)) {
throw new InsufficientStockError(item.productId, item.quantity);
}
return OrderItem.create(product, item.quantity);
});
// Create order (domain logic)
const order = Order.create(input.customerId, orderItems);
// Persist
await this.orderRepository.save(order);
// Dispatch domain events
for (const event of order.domainEvents) {
await this.eventDispatcher.dispatch(event);
}
order.clearDomainEvents();
return {
orderId: order.id.value,
totalAmount: order.totalAmount.amount,
status: order.status,
};
}
}
// src/application/services/order-service.ts
export class OrderService {
constructor(
private readonly createOrder: CreateOrderUseCase,
private readonly confirmOrder: ConfirmOrderUseCase,
private readonly cancelOrder: CancelOrderUseCase,
private readonly getOrder: GetOrderUseCase
) {}
async create(input: CreateOrderInput): Promise<CreateOrderOutput> {
return this.createOrder.execute(input);
}
async confirm(orderId: string): Promise<void> {
return this.confirmOrder.execute({ orderId });
}
async cancel(orderId: string, reason: string): Promise<void> {
return this.cancelOrder.execute({ orderId, reason });
}
async get(orderId: string): Promise<OrderDTO> {
return this.getOrder.execute({ orderId });
}
}
Infrastructure Layer
// src/infrastructure/persistence/repositories/postgres-order-repository.ts
import { Pool } from 'pg';
import { OrderRepository } from '../../../domain/interfaces/order-repository';
import { Order } from '../../../domain/entities/order';
import { OrderId } from '../../../domain/value-objects/order-id';
import { OrderMapper } from '../mappers/order-mapper';
export class PostgresOrderRepository implements OrderRepository {
constructor(private readonly pool: Pool) {}
async findById(id: OrderId): Promise<Order | null> {
const result = await this.pool.query(
`SELECT o.*, json_agg(oi.*) as items
FROM orders o
LEFT JOIN order_items oi ON o.id = oi.order_id
WHERE o.id = $1
GROUP BY o.id`,
[id.value]
);
if (result.rows.length === 0) return null;
return OrderMapper.toDomain(result.rows[0]);
}
async findByCustomerId(customerId: string, pagination: Pagination): Promise<Order[]> {
const result = await this.pool.query(
`SELECT o.*, json_agg(oi.*) as items
FROM orders o
LEFT JOIN order_items oi ON o.id = oi.order_id
WHERE o.customer_id = $1
GROUP BY o.id
ORDER BY o.created_at DESC
LIMIT $2 OFFSET $3`,
[customerId, pagination.limit, pagination.offset]
);
return result.rows.map(row => OrderMapper.toDomain(row));
}
async save(order: Order): Promise<void> {
const client = await this.pool.connect();
try {
await client.query('BEGIN');
const orderData = OrderMapper.toPersistence(order);
// Upsert order
await client.query(
`INSERT INTO orders (id, customer_id, status, total_amount, currency, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (id) DO UPDATE SET
status = EXCLUDED.status,
total_amount = EXCLUDED.total_amount,
updated_at = EXCLUDED.updated_at`,
[
orderData.id,
orderData.customer_id,
orderData.status,
orderData.total_amount,
orderData.currency,
orderData.created_at,
orderData.updated_at,
]
);
// Sync order items
await client.query('DELETE FROM order_items WHERE order_id = $1', [orderData.id]);
for (const item of orderData.items) {
await client.query(
`INSERT INTO order_items (id, order_id, product_id, quantity, unit_price, total_price)
VALUES ($1, $2, $3, $4, $5, $6)`,
[item.id, orderData.id, item.product_id, item.quantity, item.unit_price, item.total_price]
);
}
await client.query('COMMIT');
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}
async delete(id: OrderId): Promise<void> {
await this.pool.query('DELETE FROM orders WHERE id = $1', [id.value]);
}
}
// src/infrastructure/persistence/mappers/order-mapper.ts
export class OrderMapper {
static toDomain(raw: OrderRecord): Order {
const items = raw.items.map(item => OrderItem.reconstitute({
id: item.id,
productId: item.product_id,
quantity: item.quantity,
unitPrice: Money.of(item.unit_price, raw.currency),
}));
return Order.reconstitute({
id: OrderId.fromString(raw.id),
customerId: raw.customer_id,
items,
status: raw.status as OrderStatus,
totalAmount: Money.of(raw.total_amount, raw.currency),
createdAt: raw.created_at,
updatedAt: raw.updated_at,
});
}
static toPersistence(order: Order): OrderRecord {
return {
id: order.id.value,
customer_id: order.customerId,
status: order.status,
total_amount: order.totalAmount.amount,
currency: order.totalAmount.currency,
created_at: order.createdAt,
updated_at: order.updatedAt,
items: order.items.map(item => ({
id: item.id,
product_id: item.productId,
quantity: item.quantity,
unit_price: item.unitPrice.amount,
total_price: item.totalPrice.amount,
})),
};
}
}
Dependency Injection Container
// src/infrastructure/config/container.ts
import { Container, interfaces } from 'inversify';
import { Pool } from 'pg';
// Symbols for injection
export const TYPES = {
// Infrastructure
DatabasePool: Symbol.for('DatabasePool'),
RedisClient: Symbol.for('RedisClient'),
EventDispatcher: Symbol.for('EventDispatcher'),
// Repositories
OrderRepository: Symbol.for('OrderRepository'),
ProductRepository: Symbol.for('ProductRepository'),
UserRepository: Symbol.for('UserRepository'),
// Services
ProductService: Symbol.for('ProductService'),
OrderService: Symbol.for('OrderService'),
// Use Cases
CreateOrderUseCase: Symbol.for('CreateOrderUseCase'),
ConfirmOrderUseCase: Symbol.for('ConfirmOrderUseCase'),
};
export function createContainer(): Container {
const container = new Container();
// Infrastructure
container.bind<Pool>(TYPES.DatabasePool).toConstantValue(
new Pool({ connectionString: process.env.DATABASE_URL })
);
container.bind<EventDispatcher>(TYPES.EventDispatcher)
.to(InMemoryEventDispatcher)
.inSingletonScope();
// Repositories
container.bind<OrderRepository>(TYPES.OrderRepository)
.to(PostgresOrderRepository)
.inSingletonScope();
container.bind<ProductRepository>(TYPES.ProductRepository)
.to(PostgresProductRepository)
.inSingletonScope();
// Services
container.bind<ProductService>(TYPES.ProductService)
.to(ProductServiceImpl)
.inSingletonScope();
// Use Cases
container.bind<CreateOrderUseCase>(TYPES.CreateOrderUseCase)
.toDynamicValue((context: interfaces.Context) => {
return new CreateOrderUseCase(
context.container.get<OrderRepository>(TYPES.OrderRepository),
context.container.get<ProductService>(TYPES.ProductService),
context.container.get<EventDispatcher>(TYPES.EventDispatcher)
);
});
return container;
}
Usage Examples
Implement Clean Architecture
Apply backend-architecture-patterns skill to structure new service using clean architecture
Add Repository Pattern
Apply backend-architecture-patterns skill to implement repository pattern for user entity
Setup Dependency Injection
Apply backend-architecture-patterns skill to configure DI container with InversifyJS
Success Output
When this skill completes successfully, you should see:
✅ SKILL COMPLETE: backend-architecture-patterns
Completed:
- [x] Clean architecture layers implemented (domain, application, infrastructure, presentation)
- [x] Domain entities created with business logic encapsulation
- [x] Value objects implemented as immutable types
- [x] Repository interfaces defined in domain layer
- [x] Repository implementations created in infrastructure layer
- [x] Use cases implemented in application layer
- [x] Dependency injection container configured
- [x] Domain events and event dispatcher implemented
- [x] Unit tests created for domain logic
Outputs:
- src/domain/entities/ - Business entities with invariants
- src/domain/value-objects/ - Immutable value types (Money, OrderId, etc.)
- src/domain/interfaces/ - Repository interfaces
- src/application/use-cases/ - Business operations
- src/infrastructure/persistence/repositories/ - Repository implementations
- src/infrastructure/persistence/mappers/ - Domain-to-persistence mappers
- src/presentation/http/controllers/ - HTTP request handlers
- src/infrastructure/config/container.ts - DI container configuration
- tests/domain/ - Domain logic unit tests
Architecture: Clean Architecture with DDD
Layers: 4 (Domain, Application, Infrastructure, Presentation)
Dependencies: Inward-only (presentation → application → domain)
Test Coverage: 80%+ for domain logic
Completion Checklist
Before marking this skill as complete, verify:
- Directory structure matches clean architecture pattern
- Domain layer has no external dependencies (pure business logic)
- Entities encapsulate business rules with private properties
- Value objects implemented as immutable types
- Repository interfaces defined in domain layer
- Repository implementations in infrastructure layer
- Use cases orchestrate business operations in application layer
- Dependency injection container configured with all bindings
- Domain events implemented for cross-aggregate communication
- Mappers translate between domain and persistence models
- Controllers delegate to use cases (thin controllers)
- Dependencies point inward: presentation → application → domain
- Unit tests cover domain logic (entities, value objects)
- Integration tests cover repository implementations
- No circular dependencies between layers
Failure Indicators
This skill has FAILED if:
- ❌ Domain layer imports from infrastructure or presentation
- ❌ Business logic in controllers (fat controllers)
- ❌ Entities expose public properties (broken encapsulation)
- ❌ Value objects mutable (can be changed after creation)
- ❌ Repository implementations in domain layer
- ❌ Use cases directly access database (skip repository)
- ❌ No dependency injection (tight coupling)
- ❌ Domain events not implemented for aggregate communication
- ❌ Mappers missing or inconsistent
- ❌ Circular dependencies between layers
- ❌ No unit tests for domain logic
- ❌ Infrastructure concerns leak into domain
When NOT to Use
Do NOT use backend-architecture-patterns when:
- Simple CRUD API - Overkill for basic create/read/update/delete operations; use simple MVC
- Prototyping/MVP - Clean architecture adds overhead; use rapid prototyping patterns
- Microservice too small - <5 entities may not justify full architecture
- Team unfamiliar with DDD - Requires training; start with simpler patterns
- Read-only APIs - Query-focused services simpler without domain layer
- Immediate deadline - Setup time significant; use faster patterns under pressure
- Legacy codebase migration - Incremental refactoring better than full rewrite
- Very simple domain - If business logic minimal, simpler architecture appropriate
Alternative Approaches:
- MVC pattern: For simple CRUD with minimal business logic
- Transaction script: For procedural workflows without complex domain
- Active Record: For database-centric applications
- Anemic domain model: If business logic truly minimal (careful: often becomes anti-pattern)
- CQRS: For read-heavy systems with complex queries
Anti-Patterns (Avoid)
| Anti-Pattern | Problem | Solution |
|---|---|---|
| Anemic domain model | Entities just data bags, logic in services | Move business logic into entities and value objects |
| Fat controllers | Business logic in HTTP layer | Delegate to use cases, thin controllers |
| Leaky abstraction | Domain depends on infrastructure | Reverse dependency: infrastructure implements domain interfaces |
| Smart UI | Presentation logic mixed with business logic | Separate concerns: presentation formats, domain decides |
| God objects | Single entity handles too much | Apply single responsibility principle, decompose |
| Transaction script in use cases | Procedural code, missed domain modeling | Identify entities, value objects, model domain properly |
| Ignoring value objects | Primitive obsession (strings for money, IDs) | Create value objects with validation and behavior |
| Direct ORM in use cases | Domain tied to persistence | Use repository pattern, mappers isolate domain |
| No domain events | Tight coupling between aggregates | Publish domain events for aggregate communication |
| Over-engineering | Excessive abstraction for simple domain | Match architecture complexity to domain complexity |
Principles
This skill embodies these CODITECT principles:
- Separation of Concerns - Clean architecture layers isolate business logic from infrastructure
- Dependency Inversion - High-level domain doesn't depend on low-level infrastructure
- Single Responsibility - Each entity, value object, use case has one clear purpose
- Encapsulation - Entities protect invariants, enforce business rules internally
- Testability - Pure domain logic easily tested without infrastructure
- Maintainability - Clear structure makes changes predictable and safe
- Domain-Driven Design - Business concepts modeled explicitly in code
- Explicit Over Implicit - Intent clear through types, interfaces, and naming
Full Principles: CODITECT-STANDARD-AUTOMATION.md
Integration Points
- system-architecture-design - High-level architecture decisions
- microservices-patterns - Service decomposition
- database-schema-optimization - Data layer design