Specs describe the what #
In Spec-Driven Development, the specification says what the system must do, not how it does it. In practice, many specs mix both.
A typical example is a use case step like this: "The user clicks the Save button in the Vaadin Grid, and the service calls the repository." This step mixes business behavior with UI technology and internal design. A business expert cannot review it, and when you change the UI framework or the persistence layer, the spec is wrong although the business behavior is the same.
A good step says only what matters to the business: "The clerk saves the order. The system checks that the delivery date is in the future." It stays valid when the technology changes.
Leaving the how out of the spec does not mean leaving it out of the project. If nobody writes it down, the AI agent decides it, and it may decide differently each time it generates the code.
The how belongs to the guidelines #
The how belongs in the guidelines. You write them once, and every use case uses them.
Guidelines contain three things:
- Coding and design rules , for example naming, null handling, how to write a Vaadin view, how to structure a jOOQ query.
- The general architecture , for example layers, modules, Self-Contained Systems, how systems talk to each other.
- Architecture Decision Records (ADRs) , the concrete decisions with their reasons, for example "we use jOOQ, not JPA".
I split them into two levels. Company-wide guidelines are the same in every project, so a team can reuse its Spring Boot, Vaadin, and jOOQ rules. Project-specific guidelines hold decisions for one project only, for example which payment provider we call.
A guideline without a sensor is only a wish #
Writing guidelines down is not enough. An AI agent follows them most of the time and then drifts away, for example when the context gets long or when its training examples point in another direction. Developers drift too.
So every guideline needs a sensor: an automatic check that fails when the code breaks the rule.
Sensors also give the agent fast feedback. When the build fails with a clear message, the agent fixes the mistake in its next step, before a human reviews the code. So the message should say what is wrong and which rule it breaks.
Neal Ford, Rebecca Parsons, and Patrick Kua call these checks fitness functions in their book "Building Evolutionary Architectures". When AI agents write most of the code, you need them for every guideline.
Example: "We use jOOQ, not JPA" #
In my projects I use jOOQ for database access, not JPA. The ADR looks like this:
Status: Accepted
## Context
The application is database-centric. We need full control over SQL,
vendor features of Oracle and PostgreSQL, and type safety at compile time.
## Decision
All database access uses jOOQ with code generated from the schema.
JPA, Hibernate, and Spring Data JPA are not used.
## Consequences
No lazy problems and no hidden queries.
Developers must know SQL.
## Verification
- Maven Enforcer rule "adr-003-no-jpa" bans the JPA dependencies.
- ArchUnit rule "adr003NoJpa" bans the JPA packages in the code.
An AI agent that implements a new use case has seen thousands of Spring Boot examples with @Entity and JpaRepository. Without a sensor, it will sooner or later add spring-boot-starter-data-jpa. The build stays green, and nobody notices.
Sensor 1: ban the dependency
The first sensor works on the build level. The Maven Enforcer Plugin fails the build when a banned dependency appears, also as a transitive dependency.
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<executions>
<execution>
<id>adr-003-no-jpa</id>
<goals>
<goal>enforce</goal>
</goals>
<configuration>
<rules>
<bannedDependencies>
<excludes>
<exclude>jakarta.persistence:jakarta.persistence-api</exclude>
<exclude>org.hibernate.orm:*</exclude>
<exclude>org.springframework.boot:spring-boot-starter-data-jpa</exclude>
</excludes>
<message>ADR-003: We use jOOQ, not JPA. See docs/architecture/decisions/ADR-003.md</message>
</bannedDependencies>
</rules>
</configuration>
</execution>
</executions>
</plugin>
The message names the ADR and its file, so the agent knows which rule it broke and where to read the reason.
Sensor 2: ban the code
The second sensor works on the code level. An ArchUnit test checks that no class uses the JPA packages, even if they come onto the classpath through some other library.
@AnalyzeClasses(packages = "ch.martinelli.demo",
importOptions = ImportOption.DoNotIncludeTests.class)
class ArchitectureDecisionsTest {
@ArchTest
static final ArchRule adr003NoJpa = noClasses()
.should().dependOnClassesThat()
.resideInAnyPackage(
"jakarta.persistence..",
"org.hibernate..",
"org.springframework.data.jpa..")
.because("ADR-003: we use jOOQ, not JPA");
}
The because clause shows up in the failure message.
Now, when the agent adds JPA, the build fails with "ADR-003: We use jOOQ, not JPA". The agent reads the ADR and rewrites the code with jOOQ, without a human reviewer.
Make it a habit #
Every ADR and every guideline should have a sensor. Two rules help.
1. Add a Verification section to the ADR template. It names the sensor, and the sensor carries the ADR id in its name or message. This gives traceability in both directions, the same way the use case id links use cases and tests.
2. Tie the sensor to the ADR lifecycle.
- When an ADR is accepted , its sensor must exist.
- When an ADR is superseded , its old sensor is removed or replaced in the same change. Otherwise the old rule blocks the new decision, or the new decision has no protection.
- When an ADR is deprecated , its sensor goes away.
The same works for the general architecture. Module boundaries can be checked with Spring Modulith or ArchUnit. "No shared database between Self-Contained Systems" can be checked with separate database users and grants. A response time requirement can be checked with a Gatling or k6 test that has a threshold. And coding rules have their usual sensors: Checkstyle, Error Prone, NullAway with JSpecify, OpenRewrite recipes.
If every guideline names its sensor, you can see which rules are checked and which are not. The ones without a sensor are your to-do list.
When no tool can check it #
Not every guideline fits into a compiler or a test. "Use the terms from the glossary" or "keep views simple" are hard to check with a tool.
For these, an AI reviewer can be a softer sensor: a second agent checks the result against one guideline only. It is less reliable than a failing build, but it catches many problems.
Some ADRs have nothing to check, for example "We chose Vaadin because the team knows Java". Then the Verification section says "Documentation only", so readers know the sensor was not forgotten.
Conclusion #
To sum up:
- Specs define the what, in business language, so domain experts can review them and they survive technology changes.
- Guidelines , including the general architecture and the ADRs, define the how, once for all use cases.
- Sensors check that the code follows the guidelines and give the AI agent fast feedback.
The JPA example needs one ADR, one Enforcer rule, and one ArchUnit test, linked by the ADR id. Start with your most important decision and add sensors step by step.