Skip to main content

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

  1. Review the patterns and examples below
  2. Apply the relevant patterns to your implementation
  3. Follow the best practices outlined in this skill

Service design, data flow, scalability patterns, and backend system architecture.

Core Capabilities

  1. Clean Architecture - Separation of concerns
  2. Domain-Driven Design - Business logic organization
  3. Repository Pattern - Data access abstraction
  4. Service Layer - Business operation encapsulation
  5. 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-PatternProblemSolution
Anemic domain modelEntities just data bags, logic in servicesMove business logic into entities and value objects
Fat controllersBusiness logic in HTTP layerDelegate to use cases, thin controllers
Leaky abstractionDomain depends on infrastructureReverse dependency: infrastructure implements domain interfaces
Smart UIPresentation logic mixed with business logicSeparate concerns: presentation formats, domain decides
God objectsSingle entity handles too muchApply single responsibility principle, decompose
Transaction script in use casesProcedural code, missed domain modelingIdentify entities, value objects, model domain properly
Ignoring value objectsPrimitive obsession (strings for money, IDs)Create value objects with validation and behavior
Direct ORM in use casesDomain tied to persistenceUse repository pattern, mappers isolate domain
No domain eventsTight coupling between aggregatesPublish domain events for aggregate communication
Over-engineeringExcessive abstraction for simple domainMatch architecture complexity to domain complexity

Principles

This skill embodies these CODITECT principles:

  1. Separation of Concerns - Clean architecture layers isolate business logic from infrastructure
  2. Dependency Inversion - High-level domain doesn't depend on low-level infrastructure
  3. Single Responsibility - Each entity, value object, use case has one clear purpose
  4. Encapsulation - Entities protect invariants, enforce business rules internally
  5. Testability - Pure domain logic easily tested without infrastructure
  6. Maintainability - Clear structure makes changes predictable and safe
  7. Domain-Driven Design - Business concepts modeled explicitly in code
  8. 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