Architecture & Design Patterns
Clean Architecture, Hexagonal (Ports & Adapters), DDD, CQRS, Events, Outbox — senior interview-ready.
1. Principles & Boundaries
Strong architecture is about boundaries, dependency direction, and keeping business rules independent from frameworks.
✅ What good architecture buys you
- Testable domain rules (no DB/HTTP required).
- Framework independence (replace EF/MQ without rewriting business).
- Maintainability: local changes stay local.
- Clear seams for refactoring and growth.
🚨 Common trap
“Architecture = folders”. Real architecture is dependency direction + responsibilities, not just a project structure.
// Source code dependencies must point inward (toward the domain/use-cases)
Domain ← Application ← Infrastructure ← API
2. Clean Architecture
Clean Architecture focuses on layers and the dependency rule: business rules should not depend on frameworks.
✅ Typical layers
- Domain: entities, value objects, invariants, domain events
- Application: use-cases (commands/queries), ports/interfaces, orchestration
- Infrastructure: EF Core, repositories, message bus, external services
- API: controllers/endpoints, auth, filters, DTO mapping
⚠️ Trap: EF entities = Domain entities
If your domain is polluted by EF attributes, lazy-loading proxies, and persistence constraints, your architecture collapses.
// "Domain is pure rules. Application orchestrates. Infrastructure adapts. API delivers."
3. Hexagonal Architecture (Ports & Adapters)
Hexagonal expresses the same idea as Clean, but through Ports (interfaces) and Adapters (implementations).
public interface IEmailSender
{
Task SendAsync(string to, string subject, string body);
}
// Adapter (Infrastructure layer)
public class SmtpEmailSender : IEmailSender
{
public async Task SendAsync(string to, string subject, string body)
{
// SMTP implementation
await Task.Delay(10);
}
}
⚠️ Trap: “Hexa vs Clean are totally different”
They are very close: Clean is “layers & dependency rule”, Hexa is “ports & adapters”. In real code, they often converge.
4. Domain-Driven Design (DDD)
Interviewers don’t want theory only. They want: bounded contexts, aggregates, invariants, value objects, domain events.
✅ Essentials to mention
- Bounded Context = business boundary (often 1 microservice).
- Aggregate Root enforces invariants.
- Value Objects (immutable, validated, no identity).
- Ubiquitous Language shared with the business.
🚨 Traps
- Entity = DB table (false).
- Huge aggregates (lock/transaction contention).
- Anemic domain (logic outside domain only).
- Sharing domain models across contexts.
Example: Value Object + invariant
{
public decimal Amount { get; }
public string Currency { get; }
private Money(decimal amount, string currency)
{
if(amount < 0) throw new ArgumentException("Amount must be >= 0");
if(string.IsNullOrWhiteSpace(currency)) throw new ArgumentException("Currency required");
Amount = amount; Currency = currency;
}
public static Money Create(decimal amount, string currency) => new(amount, currency);
}
5. CQRS (Commands & Queries)
CQRS separates reads from writes. Mention the real reason: complex write rules + read projections + scaling.
When CQRS helps
- Complex domain invariants on writes.
- Read models/projections differ from write model.
- Different scaling needs for reads vs writes.
When CQRS is overkill
- Simple CRUD with no domain complexity.
- Team unfamiliar → maintenance risk.
Example: Command + Handler
public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, Guid>
{
private readonly IOrderRepository _repo;
public CreateOrderHandler(IOrderRepository repo) => _repo = repo;
public async Task<Guid> Handle(CreateOrderCommand cmd, CancellationToken ct)
{
var id = Guid.NewGuid();
// validate invariants here
await _repo.AddAsync(id, cmd.CustomerId, cmd.Total, ct);
return id;
}
}
🚨 Trap: “MediatR = CQRS”
MediatR is a library. CQRS is a design choice: separate responsibilities for reads/writes and possibly different models.
6. Domain Events vs Integration Events
One of the most common interview questions in event-driven designs.
Domain Event
Internal to a bounded context. Triggers internal reactions (policies, side effects) within the same service.
Integration Event
External communication (Kafka/RabbitMQ). Notifies other services. Contract must be stable and versioned.
🚨 Trap: Publishing internal domain events externally
Domain events are not contracts. Integration events are contracts. Keep them separate.
7. Transactional Boundaries & Outbox Pattern
The key question: “How do you publish events reliably without losing them?”
// 1) Begin DB transaction
// 2) Save aggregate changes
// 3) Save IntegrationEvent in Outbox table
// 4) Commit transaction
// 5) Background worker publishes Outbox messages and marks them as sent
🚨 Trap: publish directly inside the transaction
Publish before commit → you can emit events for data that never committed. Publish after commit → you can lose events on crash. Outbox provides eventual consistency + reliability.
8. Core Patterns (Interview Quick List)
✅ Core
- Factory: controlled creation + invariants
- Strategy: pluggable behavior
- Decorator: cross-cutting concerns
- Adapter: integrate external systems cleanly
- Repository: persistence for aggregates
- Unit of Work: commit boundary
💡 Distributed-friendly
- Outbox
- Idempotency (safe retries)
- Saga (orchestration/choreography)
- Circuit Breaker
- Retry with backoff
- Bulkhead
9. Decorator Pattern (Validation / Logging)
A strong “senior” answer: you keep use-cases pure and wrap cross-cutting concerns with decorators.
{
Task<TResponse> Handle(TRequest request, CancellationToken ct);
}
public class LoggingDecorator<TRequest, TResponse> : IUseCase<TRequest, TResponse>
{
private readonly IUseCase<TRequest, TResponse> _next;
private readonly ILogger _log;
public LoggingDecorator(IUseCase<TRequest, TResponse> next, ILogger log)
{ _next = next; _log = log; }
public async Task<TResponse> Handle(TRequest request, CancellationToken ct)
{
_log.LogInformation("Handling {Request}", request);
var res = await _next.Handle(request, ct);
_log.LogInformation("Handled {Request}", request);
return res;
}
}
10. Common Architecture Traps
“Repository everywhere is always good” ▼
Repository makes sense for aggregates in complex domains. For simple CRUD, it can become needless abstraction and friction.
“DDD means microservices” ▼
DDD is about modeling and boundaries. You can apply DDD in a modular monolith too.
“CQRS for every project” ▼
CQRS shines when write rules are complex or reads need special projections. Otherwise, keep it simple.
🎯 Interview Advice
Strong architecture answers are about trade-offs. Explain why you chose Clean/Hexa/DDD/CQRS, and when you wouldn’t — that’s what sounds senior.