ARCHI MASTER
Home

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.

// Dependency rule (the money sentence)
// 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.

// Interview line
// "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).

// Port (Application layer)
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 sealed record Money
{
   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 record CreateOrderCommand(Guid CustomerId, decimal Total) : IRequest<Guid>;

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?”

// Outbox (concept)
// 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.

public interface IUseCase<TRequest, TResponse>
{
   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.