Architectural Evidence in Enterprise Java: Making Domain-Driven Design Visible
Use CDI stereotypes + JMolecules annotations to make DDD architecture explicit, enforceable, and testable. This preserves design intent as your Java system evolves.
Join the DZone community and get the full member experience.
Join For FreeOne subtle challenge in software architecture is that architectural thinking can feel detached from the codebase. We draw diagrams, define layers, identify responsibilities, and craft a coherent structure — yet the moment implementation begins, those architectural ideas fade into the background. Over time, systems drift not because developers ignore design, but because the code itself provides almost no way to express that design.
This tension is well documented. In Just Enough Software Architecture, George Fairbanks argues that programming languages lack constructs for directly representing architectural concepts. Java lets us model types, fields, methods, and packages, but offers no native way to encode ideas such as “presentation layer,” “domain logic,” “aggregate root,” or “infrastructure boundary.” Without these cues in the code, architecture becomes optional, verbal, and fragile.
To bridge this gap, we need a mechanism that brings architectural intent into the codebase while preserving Java's flexibility and neutrality. That mechanism is architectural evidence.
The Missing Language of Architecture
When architects talk about software structure, they rely on a specialized vocabulary: layers, domains, flows, policies, boundaries, services, aggregates. Yet none of these concepts are represented explicitly in the programming languages we use. The language of architecture exists only in documents, conversations, and diagrams.
This mismatch produces predictable symptoms:
- A repository begins orchestrating domain rules
- A use-case class starts manipulating HTTP objects
- Entities become dependent on ORM-specific behaviors
- Infrastructure code leaks into core business logic
These issues accumulate slowly, often unnoticed at first, because the code provides no warning when they cross architectural boundaries. The language itself does not help.
This is why architectural evidence matters: it fills the semantic gap between architectural intent and implementation reality.
But before understanding how to express architecture in Java, we need to explore why expressing architecture in code is strategically essential.
Why Architectural Evidence Matters
Architecture is not merely a design artifact; it is a constraint system that guides the structure and evolution of the code. However, constraints that exist only in documentation tend to be ignored or forgotten. Constraints expressed in conversations are even more vulnerable.
Architectural evidence solves this by:
- Making architecture visible — so developers can immediately see the role of each component.
- Preserving intent — so architectural decisions live in the codebase, not in transient documents.
- Guiding decisions — so new contributors understand boundaries without guesswork.
- Enabling governance — by giving tooling a way to detect violations automatically.
- Providing longevity — ensuring the structure survives beyond team changes.
This is especially important in long-lived enterprise systems, where the architecture must outlast the individuals who created it.
Having established why architectural evidence is essential, the next question naturally follows:
Which architectural concepts should be made explicit?
Layers as First-Class Architectural Concepts
Most enterprise systems, regardless of style or technology, rely on a layered architecture. Fairbanks’ formulation is intentionally minimalistic: three layers that capture the essence of separation of concerns.
- Presentation layer: This is where interaction happens. It translates external input — HTTP requests, messages, or UI actions—into internal commands. Its purpose is communication and expression, not business logic.
- Domain layer: This is the intellectual center of the system: business rules, constraints, invariants, entities, aggregates, and domain policies. It represents knowledge rather than technology. Keeping this layer clean is the key to Domain-Driven Design (DDD).
- Support (Infrastructure) layer: This layer provides persistence, integration, messaging, and communication with external systems. It has no business semantics; instead, it offers technical capabilities.
Architects talk about these layers constantly, but the code rarely contains any explicit evidence of them. Packages give a hint, but nothing enforces or communicates a class’s architectural role.
This brings us to the practical value of architectural evidence: Annotations make these layers visible and enforceable. But before diving into how, a final conceptual step is needed: the role of testing.
Architecture as Something That Can Be Tested
Once architectural concepts become visible in code, they become testable. This transforms architecture from a document into an executable part of the system.
Tools like ArchUnit or ArchiJunit don’t invent architecture — they enforce the architecture that the team defines. With appropriate metadata, they can validate:
- Layer independence
- Domain purity
- Absence of infrastructure dependencies in core logic
- Restrictions on what repositories, controllers, and domain services can access
This is a profound shift. For the first time, architecture gains the same precision and verifiability as unit tests or API tests. Software no longer hopes to follow architecture; it proves it does.
With the conceptual groundwork in place — why architectural evidence matters, what architectural gaps it fills, and how layers shape the system — we are ready to explore the practical side: How these architectural concepts can be encoded directly in the codebase using annotations.
Show Me the Code
After understanding why architectural evidence matters, the next natural step is to see how these ideas come to life in actual code. The good news is that Jakarta EE — and specifically CDI — gives us all the pieces we need to elevate architectural concepts to first-class citizens in the codebase. Combined with JMolecules and test frameworks like ArchUnit, we can both express and validate architectural intent.
To make the example concrete, we’ll use a simple microservice in an e-commerce domain. The goal here is not to build a full application but to show how annotations become structural metadata that the architecture can rely on.
Setting Up the Environment
To start, you have two options for scaffolding the microservice:
Option 1: Create a Project Using Helidon’s MicroProfile Starter
Helidon starter:
https://helidon.io/starter/
Option 2: Use the Workshop Sample From GitHub
https://github.com/o-s-expert/ddd-workshop
Both approaches give you a clean Jakarta EE + MicroProfile base with CDI enabled.
Next, add JMolecules to your dependencies:
<dependency>
<groupId>org.jmolecules</groupId>
<artifactId>jmolecules-ddd</artifactId>
<version>${jmolecules.version}</version>
</dependency>
<dependency>
<groupId>org.jmolecules</groupId>
<artifactId>jmolecules-layered-architecture</artifactId>
<version>${jmolecules.version}</version>
</dependency>
With the molecules available, we can now create our architectural annotations.
Defining the Architectural Annotations
Each annotation is built as a CDI stereotype combined with JMolecules semantics and your architectural layer markers. This makes the annotation an architectural artifact rather than a runtime-changing feature.
Application Service
The first annotation in this tutorial helps to define the use-case orchestrators inside the code, usually known as the application service.
While classic Evans-style DDD does not explicitly define “application services,” it is more common for modern layered architectures to treat them as a clean boundary between the presentation and domain layers. This helps us separate:
- user interaction (presentation)
- business rules (domain)
- orchestration (application layer)
@Stereotype
@ApplicationScoped
@Inherited
@Retention(RUNTIME)
@Target(ElementType.TYPE)
@ApplicationLayer
public @interface ApplicationService {
}
Domain Service
As the following annotation, we have the domain service, which expresses a business rule that doesn’t naturally belong to an entity or a value object.
This guarantees that any class annotated as a domain service remains part of the business logic rather than the infrastructure.
@Stereotype
@ApplicationScoped
@Inherited
@Retention(RUNTIME)
@Target(ElementType.TYPE)
@Service
@DomainLayer
public @interface DomainService {
}
Repository
The third and final annotation we will cover today concerns implementing a repository. Naturally, we will use this annotation for implementation, not for the contract, since, as you know, the repository contract itself belongs to the domain.
Although repositories “touch” infrastructure, their concept belongs to the domain language. They sit at the boundary between domain logic and persistence technology.
@Stereotype
@ApplicationScoped
@Inherited
@Retention(RUNTIME)
@Target(ElementType.TYPE)
@DomainLayer
@InfrastructureLayer
public @interface Repository {
}
With those three annotations, you are free to test and see the intention in your code directly; thus, you can see clearly the intention of a class. This does not go against the use of terminology, such as Sufixx, to identify those layers. Still, once the goal is the semantics of the business, those annotations can help identify and archive it clearly in the code.
Validating Architecture with JMolecules + ArchUnit
Those annotations also help ensure code quality and automation. In our case, as a first step, we will use JMolecules' default behavior to verify whether the code follows the common practices of both DDD and layered applications. As you gain experience with both tools, you can increase the number of validations for this scenario.
public class JMoleculesDddUnitTest {
@Test
void checkTheLayerIntegration() {
String packageName = SimpleGreetResource.class.getPackageName();
JavaClasses classes = new ClassFileImporter().importPackages(packageName);
JMoleculesArchitectureRules.ensureLayering().check(classes);
}
@Test
void checkDDDIntegration() {
String packageName = SimpleGreetResource.class.getPackageName();
JavaClasses classes = new ClassFileImporter().importPackages(packageName);
JMoleculesDddRules.all().check(classes);
}
}
Validating Architecture With JMolecules + ArchUnit
Those annotations also help ensure code quality and automation. In our case, as a first step, we will use JMolecules' default behavior to verify whether the code follows the common practices of both DDD and layered applications. As you gain experience with both tools, you can increase the number of validations for this scenario.
public class JMoleculesDddUnitTest {
@Test
void checkTheLayerIntegration() {
String packageName = SimpleGreetResource.class.getPackageName();
JavaClasses classes = new ClassFileImporter().importPackages(packageName);
JMoleculesArchitectureRules.ensureLayering().check(classes);
}
@Test
void checkDDDIntegration() {
String packageName = SimpleGreetResource.class.getPackageName();
JavaClasses classes = new ClassFileImporter().importPackages(packageName);
JMoleculesDddRules.all().check(classes);
}
}
Conclusion
This article demonstrated how architectural evidence transforms Domain-Driven Design from a conceptual approach into a practical, maintainable structure reflected in the code. By using CDI stereotypes, we improved code semantics and clarified design intent. When combined with JMolecules, this evidence is further strengthened, allowing seamless integration with ArchUnit to create tests and achieve scalable quality within your codebase.
Opinions expressed by DZone contributors are their own.
Comments