An architecture only truly exists if it is enforced. You can document a hexagonal architecture in a wiki, draw it on a whiteboard, bring it up in code review: as long as nothing concretely prevents a developer from making the domain depend on an adapter, the boundary is nothing more than a convention. And conventions, as a team grows and pressure mounts, always end up eroding.
The real question, then, is not “how do we describe our architecture?” but “how do we make it executable?”. How do we turn a dependency rule between layers into something a machine can verify, repeatably, on every commit. That is exactly the role of architecture verification tools, and it is the practice we started applying in my team at sunday.
Why automate architecture verification
As teams grow, a gap widens between the architecture as it was intended and the architecture as it is actually implemented. Nobody deliberately introduces a forbidden dependency: it happens because a rule isn’t known, because it’s the easy path in a moment of rush, or simply because the boundary was never written down in any binding way. Once the first violation is introduced, it serves as a precedent, and the erosion accelerates.
Relying on code review alone to catch these drifts is fragile. Review depends on the vigilance of a human who must, on top of everything else, keep the project’s entire set of dependency rules in mind. This is exactly the kind of task humans are bad at and machines are excellent at: mechanically checking, without fatigue and without exception, that a set of constraints is respected.
Hence the value of tooling this verification. Rather than hoping the standards are respected, we make them impossible to break without the pipeline flagging it. The architecture then moves from the status of passive documentation to that of a testable property of the system, what is often called a fitness function: an automated function that measures whether the code respects an expected architectural characteristic.
One tool per ecosystem
The good news is that this idea is widespread enough that a mature tool exists in most ecosystems. The three we looked at closely:
- Konsist (Kotlin): verifies the structure and dependencies of Kotlin code in the form of tests, with an API dedicated to architecture rules.
- ArchUnit (Java): the reference on the JVM side, which lets you express architecture rules as JUnit unit tests.
- eslint-plugin-boundaries (JavaScript / TypeScript): an ESLint plugin that defines architectural “elements” and the allowed dependencies between them, checked directly by the linter.
The principle is the same everywhere: you declare the layers (or modules) of the architecture, then express the allowed and forbidden dependency relationships between them. The tool then walks the source code and flags any violation.
The rules of a hexagonal architecture
Let’s quickly recall the layers of a hexagonal architecture (or ports & adapters), since they are the ones we are going to constrain:
- Domain: the business core, which must depend on nothing external.
- Domain Ports: the interfaces (ports) through which the domain communicates with the outside world.
- Use Cases: the application orchestration, which builds on the domain and its ports.
- API / Primary Adapters (Left Adapters): the entry points that drive the application (HTTP controllers, message consumers…).
- SPI / Secondary Adapters (Right Adapters): the technical implementations of the ports (database access, calls to third-party services…).
In this context, the dependency rules we want to enforce are the following:
- The Domain depends neither on the Use Cases, nor on the APIs, nor on the SPIs. It’s the core: it knows nothing of the world around it.
- The APIs (Primary Adapters) depend on the domain, but do not depend directly on the SPIs.
- The SPIs (Secondary Adapters) depend on the Domain Ports and implement them. They may also depend on the APIs of another module (bounded context).
- The Use Cases depend on the domain and the Domain Ports, but do not depend directly on the SPIs.
Expressed in prose, these rules look like good intentions. The goal is to turn them into executed code.
In practice with Kotlin and Konsist
Here is how these four rules translate concretely with Konsist. You declare each layer as a Layer identified by its root package, then express the expected dependsOn / doesNotDependOn relationships. The whole thing is wrapped in a test: it will therefore be run as often as the test suite, both locally and in continuous integration.
class ArchitectureRules {
@Test
fun `Architecture dependencies rules`() {
Konsist.scopeFromProduction()
.assertArchitecture {
// Infrastructure, Primary Adapters (API) and Secondary Adapters (SPI)
val apiLayer = Layer(name = "Infrastructure_API", rootPackage = "com.<package>..infrastructure.api..")
val spiLayer = Layer(name = "Infrastructure_SPI", rootPackage = "com.<package>..infrastructure.spi..")
// Domain
val domainLayer = Layer(name = "Domain", rootPackage = "com.<package>..domain..")
val domainPortLayer = Layer(name = "Domain_Ports", rootPackage = "com.<package>..domain..ports..")
// Use Cases
val useCases = Layer(name = "Use_Cases", rootPackage = "com.<package>..usecases..")
// API and SPI are the ones depending on domain layer and domain ports
apiLayer.dependsOn(domainLayer)
apiLayer.doesNotDependOn(spiLayer)
spiLayer.doesNotDependOn(apiLayer)
spiLayer.dependsOn(domainPortLayer, apiLayer)
// Use cases should depend on domain layer and domain ports, but not on API or SPI
useCases.dependsOn(domainLayer, domainPortLayer)
useCases.doesNotDependOn(apiLayer, spiLayer)
// Domain does not depend on API or SPI
domainLayer.doesNotDependOn(apiLayer, spiLayer)
domainPortLayer.doesNotDependOn(apiLayer, spiLayer)
}
}
}
What’s interesting here is the direct correspondence between intent and code. The comment “Domain does not depend on API or SPI” reads immediately in the two calls domainLayer.doesNotDependOn(...) and domainPortLayer.doesNotDependOn(...) that follow it. There is no longer any possible gap between the rule as stated and the rule as verified: they are one and the same thing. The day someone introduces an import from the domain to an adapter, this test fails, and the drift is stopped before it even reaches the main branch.
It is this property that gives the approach its value: the architecture becomes a test, that is, an active safety net, and no longer a document consulted with hope.
The feedback loop could go further
Integrating these rules into the test suite is already a considerable step forward: tests are run very frequently, both locally and in the continuous integration pipeline, so a violation is detected quickly. It’s an excellent first step.
Ideally, though, we’d want to shorten the feedback loop even further. Rather than waiting for the tests to run, we’d like these rules to be flagged directly in the IDE, as the code is being written, in the manner of an ESLint. On the Kotlin side, this would go through a linter like Detekt: the architectural error would then appear as an immediate warning, right where the bad dependency is introduced, instead of being discovered after the fact.
This is, by the way, a natural advantage of eslint-plugin-boundaries in the JavaScript / TypeScript ecosystem: being an ESLint plugin, it provides this instant feedback in the editor. The lesson is that the value of an automated architecture rule grows with how quickly it surfaces the information. The earlier the signal arrives, the cheaper the fix.
Conclusion
Enforcing an architecture with code means refusing to let the most important boundaries of a system rest on collective discipline alone. Konsist, ArchUnit and eslint-plugin-boundaries share the same promise: turning dependency rules stated in prose into constraints verified by the machine, repeatably and without exception.
The benefit isn’t measured only in bugs avoided, but in clarity preserved over time. A hexagonal architecture whose layers are checked automatically stays faithful to its original intent even after dozens of contributors and hundreds of commits. Starting with architecture tests is simple and accessible; aiming next for feedback in the IDE is the next step up. In both cases, the principle remains the same: an architecture you make executable is an architecture that lasts.