with one click
backend-principles
// Use when: designing backend structure, organizing business logic, choosing layers, separating controller/service/repository responsibilities, or deciding whether DDD is warranted.
// Use when: designing backend structure, organizing business logic, choosing layers, separating controller/service/repository responsibilities, or deciding whether DDD is warranted.
[HINT] Download the complete skill directory including SKILL.md and all related files
| name | backend-principles |
| description | Use when: designing backend structure, organizing business logic, choosing layers, separating controller/service/repository responsibilities, or deciding whether DDD is warranted. |
| user-invocable | false |
Use Layered Architecture instead of traditional MVC. Each layer has a clear responsibility:
| Layer | Responsibility |
|---|---|
| Controller | Receives requests, validates input, orchestrates multiple services to fulfill business flows, returns responses |
| Service | Single-responsibility business logic unit — does NOT call other services |
| Repository | Data access abstraction, encapsulates DB query logic |
Each service owns exactly one business capability and only accesses repositories/gateways within its own domain. A service may contain business logic (calculations, validations, transformations), but must NEVER orchestrate a multi-step business flow or call other services.
Wrong — service orchestrates the entire business flow:
class OrderService {
// BAD: service is orchestrating — checking inventory, charging payment,
// creating order, and sending notifications. This is controller's job.
async createOrder(dto: CreateOrderDto) {
const inventory = await this.inventoryRepo.check(dto.items);
if (!inventory.available) throw new ConflictException('Out of stock');
const payment = await this.paymentGateway.charge(dto.amount);
const order = await this.orderRepo.create({ ...dto, paymentId: payment.id });
await this.notificationService.send(order); // BAD: service calling service
return order;
}
}
Correct — each service owns its domain logic, controller orchestrates the flow:
class OrderService {
// Service owns order-specific business logic
async create(items: Item[], paymentId: string): Promise<Order> {
const totalAmount = items.reduce((sum, i) => sum + i.price * i.quantity, 0);
const order = Order.build({ items, paymentId, totalAmount, status: 'created' });
return this.orderRepo.save(order);
}
}
class InventoryService {
// Service owns inventory-specific business logic
async reserve(items: Item[]): Promise<ReservationResult> {
const availability = await this.inventoryRepo.checkBatch(items);
const unavailable = items.filter((i) => !availability.has(i.sku));
if (unavailable.length) return { success: false, unavailable };
await this.inventoryRepo.reserveBatch(items);
return { success: true, unavailable: [] };
}
}
class PaymentService {
async charge(amount: number, method: PaymentMethod): Promise<PaymentResult> {
return this.paymentGateway.process(amount, method);
}
}
// Controller orchestrates services to implement the business flow
class OrderController {
async createOrder(req: Request, res: Response) {
const reservation = await this.inventoryService.reserve(req.body.items);
if (!reservation.success) return res.conflict({ unavailable: reservation.unavailable });
const payment = await this.paymentService.charge(req.body.amount, req.body.method);
if (!payment.success) return res.paymentRequired('Payment failed');
const order = await this.orderService.create(req.body.items, payment.id);
await this.notificationService.notify(req.user, order);
return res.created(order);
}
}
If a controller method grows too long (30-40+ lines), extract it into a use case or workflow. A use case is an orchestration layer — it composes services to fulfill a specific business flow, just like a controller does, but as a standalone unit.
// Use case: orchestration extracted from controller
class CreateOrderUseCase {
async execute(input: CreateOrderInput): Promise<Order> {
const reservation = await this.inventoryService.reserve(input.items);
if (!reservation.success) throw new ConflictException('Out of stock');
const payment = await this.paymentService.charge(input.amount, input.method);
if (!payment.success) throw new PaymentRequiredException('Payment failed');
const order = await this.orderService.create(input.items, payment.id);
await this.notificationService.notify(input.userId, order);
return order;
}
}
// Controller becomes a thin HTTP adapter
class OrderController {
async createOrder(req: Request, res: Response) {
const order = await this.createOrderUseCase.execute(req.body);
return res.created(order);
}
}
DDD is a design philosophy centered on the business domain — not a pattern, but a way of thinking.
Core idea:
Structure software to reflect real business rules, not around databases or frameworks.
The more items you match, the more DDD (at least lightweight) is worth adopting:
| Scale | Strategy |
|---|---|
| Small | Layered Architecture + Controller/Service/Repository, modularize as needed |
| Medium | Lightweight DDD — introduce Entity/Value Object, move core rules into Domain Model |
| Large / High complexity | Full DDD — Bounded Context, Aggregate, Domain Event, paired with Clean/Hexagonal Architecture |
DDD addresses business complexity, not role count or folder structure. Without business complexity, DDD only adds development cost.