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)

DomainApplicationInfrastructureAPI

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.