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
PublicDataDtoduplicates a few properties of an entity, so that a peer never holds the entity itself. - Learning the vocabulary. Driving and driven ports,
.Abstractionsversus.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
.Modelsand.Abstractionsif you never expect another caller than the Web. You lose the guarantee that.Abstractionsexposes nothing but contracts. - Skip the read side for a trivial screen and read through the write repository.
ReadSideBoundaryTestswill 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.