Tuesday, October 28, 2025

Architecture - Clean | Hexagonal | Onion Architecture

Clean Architecture (Uncle Bob)

Idea (one-liner)

Structure your app into concentric rings where inner rings (entities / use-cases) contain enterprise/business rules and outer rings contain frameworks and UI. Dependencies always point inward.



Layers (outer → inner)

  • Infrastructure (EF, DB, web frameworks, email)
  • Interface Adapters (controllers, presenters, gateways) — translate to/from domain
  • Use Cases / Application Services (interactors)
  • Entities / Domain (business rules, value objects, aggregates)

Key rules

  • Inner layers are framework-agnostic.
  • Use dependency inversion: inner layer defines interfaces; outer layers implement them.
  • Controllers and UI are at the outermost layer.

Typical folder structure (C#)

/src /MyApp.Api -> Web API (Controllers, DTOs, ASP.NET) /MyApp.Application -> Use cases / Application services / Interfaces for Repos /MyApp.Domain -> Entities, ValueObjects, DomainEvents, Interfaces (rare) /MyApp.Infrastructure -> EF Core, repository implementations, messaging, persistence /MyApp.Tests

Tiny C# example (Clean)

Domain (inner):

// MyApp.Domain/Order.cs public class Order { /*aggregate root*/ } public interface IOrderRepository { Order GetById(Guid id); void Save(Order order); }

Application (use-case):

// MyApp.Application/PlaceOrderService.cs public class PlaceOrderService { private readonly IOrderRepository _orders; public PlaceOrderService(IOrderRepository orders) => _orders = orders; public void Execute(PlaceOrderCommand cmd) { var order = new Order(/*...*/); // business/use-case logic _orders.Save(order); } }

Infrastructure (outer):

// MyApp.Infrastructure/OrderRepository.cs public class OrderRepository : IOrderRepository { private readonly AppDbContext _db; public OrderRepository(AppDbContext db) => _db = db; public Order GetById(Guid id) => _db.Orders.Include(...).FirstOrDefault(o=>o.Id==id); public void Save(Order order) { _db.Orders.Update(order); _db.SaveChanges(); } }

API wiring (composition root):

// MyApp.Api/Program.cs services.AddScoped<IOrderRepository, OrderRepository>(); services.AddScoped<PlaceOrderService>();

Onion Architecture (Jeffrey Palermo)

Idea (one-liner)

Very similar to Clean: domain model in the center, application services around it, and infrastructure at the outermost ring. Emphasis on domain-centric design and keeping domain independent of data concerns.



Layers

  • Domain (entities, value objects, interfaces)
  • Application Services (business use-cases)
  • Infrastructure (implementations)
  • Presentation (UI, API) — sometimes shown outside infrastructure

Key rules

  • Dependencies point toward the domain core.
  • Domain defines repository interfaces; implementations reside in Infrastructure.
  • Typically slightly simpler conceptual mapping than Clean but equivalent in practice.

Folder structure

/src /Domain /Application /Infrastructure /Web

C# example differences

Code examples are essentially identical to Clean — both use dependency inversion and keep domain stable. Onion often expresses the idea with an emphasis on domain-first development.


Hexagonal Architecture (Ports & Adapters — Alistair Cockburn)

Idea (one-liner)

Design your app around the core domain + application logic and expose ports (interfaces). External systems connect via adapters (implementations). The core is decoupled from I/O and UIs.

Structure

  • Domain / Application core (inside)

    • Defines ports (interfaces): e.g., IOrderRepository, IEmailSender

  • Adapters (outside)

    • Primary adapters: UI / CLI / REST controllers (call the core)
    • Secondary adapters: DB, message buses, email (implement ports)

Key rules

  • The core depends only on ports (interfaces).
  • Adapters implement ports and are swappable (DB → Mongo/SQL without changing core).
  • Great for systems where many different input/output mechanisms exist.

Folder structure

/src /Core -> Domain, Ports (interfaces), Use Cases /Adapters /Rest -> Controllers calling use-cases /Ef -> EF Core implementation of repository port /Messaging

C# example (port/adapter style)

Core (port definition):

// Core/Ports/IOrderRepository.cs public interface IOrderRepository { Order Load(Guid id); void Store(Order order); }

Adapter (EF):

// Adapters/Ef/OrderRepositoryEf.cs public class OrderRepositoryEf : IOrderRepository { /*implements using DbContext*/ }

Controller (primary adapter):

// Adapters/Rest/OrdersController.cs [HttpPost] public IActionResult Create(OrderDto dto) { var order = _mapper.Map<Order>(dto); _placeOrderUseCase.Execute(order); return Ok(); }

Visual / conceptual differences (short)

  • Clean vs Onion: almost same in practice. Clean uses explicit layers named “entities/use cases/controllers”, Onion emphasizes domain core and rings. Both use dependency inversion.
  • Hexagonal: emphasizes ports & adapters — think of core exposing ports and everything else being adapters. It’s focused on input/output boundaries rather than strict concentric layers.

When to choose which

  • Small/Medium app / DDD approach — Onion or Clean both work well; pick the vocabulary your team prefers.
  • Many different input/output channels (CLI, REST, event bus, scheduled jobs) — Hexagonal is a natural fit because it models ports/adapters explicitly.
  • Strict separation and testability — All three help, but Hexagonal makes adapter-swapping explicit which is great for testing with fakes.

Mapping to DDD concepts (your earlier list)

  • Aggregates: live in Domain (core). Repositories in Application or Domain define interfaces that operate on Aggregates.
  • Repositories: interface (port) in Domain or Application layer; implementation in Infrastructure/Adapters.
  • Domain Events: defined in Domain; published from Aggregates; handled by handlers in Application or Infrastructure (depending on side-effects).
  • DTOs: used at outer layers (API/presentation) to transport data in/out. Map to/from domain objects in Interface Adapters / Controllers.
  • Bounded Contexts (BC): each BC can be its own Clean/Onion/Hexagonal application — maintain separate domain models and services; integration via well-defined anti-corruption layers or translation adapters.

Concrete interview-ready comparisons (table)

ConcernCleanOnionHexagonal
Core ideaInner business rules, outer frameworksDomain-first ringsPorts (interfaces) and adapters
DependenciesInward, DI + interfacesInward, domain-centricCore depends only on ports
Where repos liveInterface in inner, impl outerInterface inner, impl outerPort in core, adapter implements port
Best forGeneral large appsDomain-driven designSystems with many I/O channels
TestabilityExcellentExcellentExcellent + explicit adapter fakes
Typical complexityMediumMediumSlightly higher modeling upfront

Extra: short, realistic example — where to put code

  • Domain (MyApp.Domain)

    • Order, OrderItem, Money (value object)
    • OrderPlacedEvent
    • IOrderRepository (or Ports/OrderRepository in Hexagonal)
  • Application (MyApp.Application)

    • Use-cases: PlaceOrder, CancelOrder
    • Domain event dispatching orchestration (or decoupled into a DomainEvents mechanism)
  • Infrastructure (MyApp.Infrastructure)

    • EfOrderRepository implements IOrderRepository
    • EmailSender implements IEmailSender
  • API / UI (MyApp.Api)

    • Controllers, DTOs, mappers to domain

  • Composition Root

    • Wire DI: register repositories, event bus, mapper, use-cases


Practical tips / gotchas

  1. Don't put EF attributes in domain objects if you want a pure domain model. If you prefer convenience, accept slight coupling.
  2. Keep domain logic inside domain (rich model) and avoid anemic models that move rules into services.
  3. Define repository interfaces in the domain (or core) so implementations can be swapped.
  4. Treat DTOs as translation objects — no business logic in DTOs.
  5. Bounded Contexts: prefer separate projects/apps when models diverge significantly — use anti-corruption layers for translations.
  6. Start pragmatic: very small projects may be over-architected if you create many projects — aim for clarity and testability rather than perfect structure.

No comments:

Post a Comment

CI/CD - Safe DB Changes/Migrations

Safe DB Migrations means updating your database schema without breaking the running application and without downtime . In real systems (A...