Code examples follow in a later revision.
On this page
Architectural properties addressed
- P-1A sealed hierarchy that separates proposals from verified values, validation at the web boundary, and module rules that the build verifies.
- P-4Micrometer Observation and Tracing for execution context, the audit identity in every contract, and audit entries written inside the business transaction.
- P-5Provenance as fields of the value types, serialised explicitly.
- P-6Architecture verification in the test phase, and required checks that cannot pass by being skipped.
- P-7Ports in the domain module and adapters in modules of their own.
- P-8A policy interface with an engine adapter, and decisions recorded with their revision.
- P-2, P-3Only at the depth COADF publishes.
Architectural intent
Spring's strength for these properties is declaration: constraints on types, transactions and validation applied by the container, observations configured once, module rules checked by a test. Its characteristic failure is the other side of the same design. A declaration holds on the paths the container intercepts, through a proxy, at a web binding or on an executor it configured, and nowhere else. A property written as an annotation is only as strong as the set of calls that actually go through Spring.
This profile maps each property onto Spring Boot 4 and Spring Framework 7, the current generation when it was written, and names the places where a declared property quietly stops applying.
Technology mapping
| Architectural property | Java and Spring |
|---|---|
| Boundary type | A sealed interface with record implementations: a proposal type and a verified type, constructed in different modules. Records are shallowly immutable carriers of data: a component that is a mutable list stays mutable, so copy it in the constructor. Sealed classes are a standard feature since Java 17. |
| Runtime validation | Jakarta Validation constraints on request types, triggered with @Valid. A failed @RequestBody validation raises MethodArgumentNotValidException, answered with 400 by default. |
| Unknown fields | An explicit, tested decision on boundary types about properties the type does not declare, rather than whatever the JSON mapper's configuration currently is. |
| Architecture rule | Spring Modulith's ApplicationModules.verify(), or ArchUnit rules written as unit tests. |
| Execution context | Micrometer Observation and Micrometer Tracing, bridged to OpenTelemetry. |
| Asynchronous context | ContextPropagatingTaskDecorator with the Micrometer context-propagation library; for the auto-configured executor, the Boot property spring.task.execution.propagate-context. |
| Audit trail | Entries written in the business transaction; database grants and triggers as in the PostgreSQL example; Spring Modulith's event publication registry for events that must not be lost. |
| Standards isolation | Port interfaces in the domain module; one adapter module per external system; the vendor client never visible to the domain. |
| Policy | A domain-owned policy interface with an Open Policy Agent adapter, or rule tables; the revision stored on the decision. |
| Integration tests | A real PostgreSQL through Testcontainers, connected with @ServiceConnection. |
Reference pattern
Code examples follow in a later revision. They are deferred until a reviewer working in this ecosystem has read them, rather than written to make the four profiles look alike. The structure below is the reference pattern those examples will implement.
Modules
inferencecalls the model and returns aProposal, a record carrying the value, the method and the provenance. It has no dependency onrecordsoraudit, and a Modulith verification or an ArchUnit rule says so in a test.recordsis the only module that can create aVerifiedValue. The type is not public outside the module, or it is a class whose constructor is not: a public record cannot hide its constructor, because the canonical constructor of a public record must itself be public. Creation goes through a factory that requires the verification of a named person.auditappends entries with the published fields, inside the transaction of the change it records, through a repository that offers no update or delete.classificationdeclares the port;classification.vendorimplements it, and is the only module that can see the vendor's client or its types.policydeclares the policy interface and the decision record; an adapter module talks to the engine.
Boundaries at run time
Validation happens at the web binding, where Spring evaluates constraints on the request body, and again inside the domain factory, because the web binding is one entry point among several: a message listener, a batch job and a test all reach the domain without passing it.
Observations wrap the boundary and the external calls. Every executor that runs work for a request is configured to propagate context, and the audit identity is a field of every message and job argument, never recovered from a thread-local after the fact.
Failure modes
Self-invocation bypasses the proxy
Spring AOP is proxy-based: a call from one method of a bean to another method of the same bean does not pass through the proxy, so method validation and
@Transactionaldo not apply to it. A constraint that holds for callers outside the bean does not hold for the bean itself, which is often where the batch path starts.Constraints declared, never evaluated
Jakarta Validation constraints on a type do nothing by themselves. They are evaluated where something triggers validation:
@Validor@Validatedat a web binding, or method validation through the proxy. A message listener that receives the same type validates nothing unless it asks.Undeclared properties left to the mapper's configuration
Whether an incoming JSON document may carry properties the type does not declare is a setting of the JSON mapper. For a boundary type, the right answer is usually to refuse them, and that decision belongs on the type and in a test, not in an application property somebody else can change.
Context lost on
@Asyncand custom executorsPropagating observation context to other threads needs a
ContextPropagatingTaskDecoratorand the context-propagation library; for the auto-configured executor, Spring Boot makes it opt-in throughspring.task.execution.propagate-context, which is off by default. Work handed to a thread pool then runs without its trace.ORM immutability taken for an audit safeguard
Hibernate ignores in-memory changes to a managed
@Immutableentity, with no update and no exception. Bulk updates against it throw by default in Hibernate 7 and only warned in 6.6. None of this stops a second client, a script or a migration from updating the table; the protection belongs in the database.Audit events lost after commit
An event handled after the transaction commits is lost if the handler fails, unless something recorded it. Spring Modulith's event publication registry writes an entry in the publishing transaction and keeps failed publications for resubmission; republishing them automatically on restart is a setting you have to switch on.
A public record as the verified type
A public record's canonical constructor must be public, so any code that can see the type can build a verified value without a verification. Keep the type out of reach, or use a class with a private constructor and a factory.
Two tracing bridges
Micrometer Tracing bridges to Brave or to OpenTelemetry, and its documentation asks you to pick only one. Two on the classpath produce traces that depend on which one wins.
Integration tests against a different database
Grants, triggers and constraint behaviour are properties of the real database. Tests against an embedded substitute verify the code and not the property.
Verification
Unit test
Passes when: The verified type can be created only through the factory, and only with a verification by a named person; a rejection leaves the attribute empty.
Proof of teeth: Make the verified type public in a scratch branch: a test that asserts it is not accessible from the inference module fails.
Architecture test
Passes when: Spring Modulith's verification passes, or the ArchUnit rules do: no dependency from
inferencetorecordsoraudit, no vendor type in the domain. ArchUnit checks dependencies between packages and classes as plain unit tests, and Modulith's verification rejects cycles between modules and references into another module's internal packages, except into modules declared open. Either tool checks the rules written down; whether they are the right rules is the team's design.Proof of teeth: Plant the forbidden dependency: the verification fails.
Integration test
Passes when: Against PostgreSQL from Testcontainers: the application's role cannot update or delete the trail, and entries are written in the same transaction as the change.
Proof of teeth: Roll back the business transaction after writing: the audit entry must not survive it.
Integration test
Passes when: Context survives the executors the application uses: an in-memory exporter shows the asynchronous work as part of the same trace, and the audit identity is present in its record.
Proof of teeth: Remove the task decorator: the test fails.
Contract test
Passes when: Producer and consumer agree on the proposal and decision contracts, with the provenance and the revision as required fields.
End-to-end test
Passes when: An output whose attribute has not been verified cannot be published, whatever entry point the request came through.
Alternative realizations
- Other Java frameworks (Quarkus, Micronaut, Jakarta EE) express the same boundary with different interception models; the self-invocation question has to be asked of each.
- Explicit SQL (jOOQ or plain JDBC) for the audit trail instead of an ORM, where the exact statements matter more than mapping convenience.
- Event sourcing frameworks where the history is the model.
- Rule tables owned by the application instead of an external policy engine, with the same revision requirement.
Trade-offs
- Declarative is concise and invisible. A reader cannot see from the call site whether a constraint or a transaction applies; tests have to show it.
- Spring Modulith encodes conventions, which is fast to adopt and opinionated; ArchUnit is general and needs the rules written out.
- JPA is convenient and puts a layer between the code and the database's own protections. For the audit trail, the layer is the part you have to see through.
- Module boundaries inside one deployable keep the operational model simple, and make the boundary depend entirely on the build's verification.
Limitations
- No code is published for this profile in Companion 1.0, and no example was executed. The behaviour described is taken from the current first-party documentation, checked on 11 September 2026, for Spring Boot 4.1, Spring Framework 7.0, Spring Modulith 2.1, Hibernate ORM 7.4 and Java SE 25.
- Nothing here shows how confidence is represented, when review is required, or how review is organised; COADF does not publish those parts of P-2 and P-3.
- Everything cited is language or framework behaviour. The architectural property, that a proposal cannot become a record without a verification, is a design the team still has to make and test: no annotation, record or module rule provides it by itself.
What this profile does not establish
Following this profile does not establish regulatory compliance, certification or conformity assessment, and Spring is not required by COADF.
Tested reference environment
No example was executed for this profile.
Sources
- Oracle: java.lang.Record · language documentation · Java SE 25 · Checked on 2026-09-11
- OpenJDK: JEP 409: Sealed Classes · specification · Checked on 2026-09-11
- Oracle: Java Language Specification: canonical constructors of record classes · specification · Java SE 25 · Checked on 2026-09-11
- Spring Framework: @RequestBody validation · official documentation · Spring Framework 7.0 · Checked on 2026-09-11
- Spring Framework: Understanding AOP proxies · official documentation · Spring Framework 7.0 · Checked on 2026-09-11
- Spring Framework: Spring-driven method validation · official documentation · Spring Framework 7.0 · Checked on 2026-09-11
- Spring Framework: Observability support: context propagation · official documentation · Spring Framework 7.0 · Checked on 2026-09-11
- Spring Boot: Observability: context propagation · official documentation · Spring Boot 4.1 · Checked on 2026-09-11
- Spring Boot: Tracing · official documentation · Spring Boot 4.1 · Checked on 2026-09-11
- Spring Boot: Testcontainers service connections · official documentation · Spring Boot 4.1 · Checked on 2026-09-11
- Spring Modulith: Verifying application module structure · official documentation · Spring Modulith 2.1 · Checked on 2026-09-11
- Spring Modulith: Event publication registry · official documentation · Spring Modulith 2.1 · Checked on 2026-09-11
- Hibernate ORM: Immutable entities · official documentation · Hibernate ORM 7.4 · Checked on 2026-09-11
- ArchUnit: ArchUnit user guide · official documentation · ArchUnit 1.5 · Checked on 2026-09-11
- Micrometer: Micrometer Tracing: supported tracers · official documentation · Micrometer Tracing 1.7 · Checked on 2026-09-11
