Careeroza
Single Responsibility
Thin Controllers, Focused Services
Each class should have one reason to change.
| Layer | Does | Does NOT |
|---|---|---|
| Controller | Read params/body, call a service, return the result | SQL, price calculation, sending email |
| Service | Business rules (stock check, discount, order total) | Know about HTTP request/response objects |
| Repository | Read and write the database | Business rules |
Bad vs Good
Bad: the controller does everything.
@Controller('orders')
export class OrdersController {
@Post()
async placeOrder(@Body() dto: CreateOrderDto) {
const product = await this.db.query('SELECT * FROM products WHERE id = $1', [dto.productId]);
if (product.stock < dto.quantity) throw new BadRequestException('Out of stock');
const total = product.price * dto.quantity * 1.18;
await this.db.query('INSERT INTO orders ...');
await this.mailer.send(dto.email, 'Order placed');
return { total };
}
}Good: the controller delegates, the service orchestrates smaller services.
// orders.controller.ts
@Controller('orders')
export class OrdersController {
constructor(private readonly ordersService: OrdersService) {}
@Post()
placeOrder(@Body() dto: CreateOrderDto) {
return this.ordersService.place(dto);
}
}
// orders.service.ts
@Injectable()
export class OrdersService {
constructor(
private readonly products: ProductsService,
private readonly pricing: PricingService,
private readonly ordersRepo: OrdersRepository,
private readonly notifications: NotificationsService,
) {}
async place(dto: CreateOrderDto) {
const product = await this.products.reserveStock(dto.productId, dto.quantity);
const total = this.pricing.orderTotal(product.price, dto.quantity);
const order = await this.ordersRepo.save({ ...dto, total });
await this.notifications.orderPlaced(order);
return order;
}
}