Hexagonal architecture

The problem it solves

A SaaS back end grows by accretion. In a layered solution, every feature lands in the same Application and Infrastructure projects, and nothing stops a service from reaching into another feature's tables "just this once". A few years in, a change to billing breaks invitations, and nobody can say which code depends on what.

The Starter answers with two things:

  • Ports and adapters. The domain declares what it needs (a repository, a mailer) as interfaces it owns; technology plugs in from outside. Replacing SMTP, the bus or the file store means rewriting one adapter, not touching a domain.
  • One project per domain, with two doors between domains — a small public contract and integration events — and architecture tests that fail the build when a reference points the wrong way. The boundaries survive because they are compiled, not because a reviewer remembered them.

What it costs

  • More projects. A domain is up to four projects plus its tests; the solution has over a hundred. Builds parallelise, so the cost in time is small, but there is more to navigate.
  • Ceremony for small features. Adding a field that the screen shows may touch the model, the EF configuration, the write input, the read projection, the DTO and the Angular model.
  • Copies on purpose. A PublicDataDto duplicates a few properties of an entity, so that a peer never holds the entity itself.
  • Learning the vocabulary. Driving and driven ports, .Abstractions versus .PublicData, read side versus write side.

What you can relax

  • Fewer projects per domain. A small domain that no peer consults does not need a .PublicData — Notifications has none.
  • Merge .Models and .Abstractions if you never expect another caller than the Web. You lose the guarantee that .Abstractions exposes nothing but contracts.
  • Skip the read side for a trivial screen and read through the write repository. ReadSideBoundaryTests will not object to a service reading its own repository, but you lose projections and paging.

What not to relax: the architecture tests. If a rule is in the way, change the rule in Blouppy.Architecture.UnitTests deliberately — routing around it is how the boundaries erode.