Overview

The back end is hexagonal — ports and adapters — rather than a stack of layers. The domain sits in the middle and declares the interfaces it needs; everything technical is an adapter plugged into one of those interfaces, and every reference points inward.

The difference shows in the questions you ask. A layered reading asks "which layer is this in?" and grows one Application project that every feature ends up in. A hexagonal reading asks "whose rule is this?" and "is this a port or an adapter?", and produces one project per domain, with the technical world outside, replaceable.

Driven adapters — they get called

Inside the hexagon

Driving adapters — they call in

implemented by

Web API controllers
(behind the gateway)

Message consumers

MCP server · Assistant

Driving ports
IXxxService
IXxxApiQueryService

Domains and modules
services · validators · handlers

Models
entities · value objects · events

Driven ports
IXxxRepository · IEmailSender
IFileStorage · IResponseCache …

Database · EF Core

Caching · Redis

Messaging · Wolverine

Email · Files · SSE · Jobs · LLM …

Driven adapters — they get called

Inside the hexagon

Driving adapters — they call in

implemented by

Web API controllers
(behind the gateway)

Message consumers

MCP server · Assistant

Driving ports
IXxxService
IXxxApiQueryService

Domains and modules
services · validators · handlers

Models
entities · value objects · events

Driven ports
IXxxRepository · IEmailSender
IFileStorage · IResponseCache …

Database · EF Core

Caching · Redis

Messaging · Wolverine

Email · Files · SSE · Jobs · LLM …

Solid arrows are calls. The dotted one is the twist that gives the pattern its value: a driven adapter implements a port the domain declares, so the code depends on the port — the domain never references Wolverine, EF Core or SMTP.

The vocabulary

TermMeaning hereWhere it lives
Driving portWhat the outside may ask the domain to doBlouppy.<Domain>.Abstractions (IWorkspacesService), Blouppy.ApiQuery.Abstractions for reads
Driven portWhat the domain needs from the outsideBlouppy.Infrastructure.<Concern>.Abstractions (IWorkspacesRepository, IEmailSender)
Driving adapterTurns a request into a call on a driving portBlouppy.Web, the message consumers, the MCP server
Driven adapterImplements a driven port with a real technologyBlouppy.Infrastructure.<Concern>
Composition rootThe one place that wires adapters to portsBlouppy.Web (AddDomainServices(), AddInfrastructureServices())

A driven port has no Service suffix: it is IWorkspacesRepository, not IWorkspacesRepositoryService.

Three rules

  1. A port lives in an .Abstractions project. The domain declares what it needs — a repository, a file store, a mailer — and never sees who implements it.
  2. One project per domain, and domains do not touch. A domain needing a fact about another goes through that domain's .PublicData contract or an integration event — never a peer's repository, service or entity.
  3. Reads and writes are separate verticals. A GET resolves through Blouppy.ApiQuery and its projections; everything else goes through the domain's own service and returns a ValidationResult<T>.

These rules are not left to code review. Blouppy.Architecture.UnitTests reads the compiled assemblies and fails dotnet test when a reference points the wrong way:

RuleTest class
Domains do not reference one another, nor the infrastructure, the Web or the read sideDomainBoundaryTests
Only the read side injects a read repository; read projections stay in ApiQuery.ModelsReadSideBoundaryTests
Each adapter references only the projects its row allows; no port references an adapterInfrastructureBoundaryTests
Nothing outside the messaging adapter references WolverineBusLibraryBoundaryTests
Every write of a driving port is audited, or says why notAuditCoverageTests

The map

back/
├── Blouppy.SharedKernel/              base entities, typed ids, results, value objects
├── Blouppy.<Domain>[.Models]          one set per domain: Workspaces, Organizations,
│   [.Abstractions][.PublicData]       Users, Notifications
├── Blouppy.ApiQuery[.Abstractions]    the read side: projections, filters, paging
│   [.Models]
├── Blouppy.Billing, .Privacy,         modules: shaped like a domain,
│   .Webhooks, .Audit, .Ai             each owning a feature
├── Blouppy.Infrastructure.<Concern>   one driven adapter each, its port in .Abstractions
├── Blouppy.Web/                       the HTTP adapter and composition root
├── Blouppy.AgentTools, .Mcp,          the tools shared by the MCP server and the assistant
│   .Assistant
├── Blouppy.Gateway/                   YARP + the session cookie (the BFF)
├── Blouppy.Identity/                  the OpenID Connect server, its own hexagon
├── Blouppy.MigrationRunner/           applies every migration, then exits
└── Blouppy.Aspire.AppHost/            the development graph

Each piece has its own page: