Your Java Architecture Is Lying to You: Enforcing Real Boundaries with Quarkus
How executable architecture tests with Taikai replace diagrams, reviews, and tribal knowledge in modern Java systems.
Most Java teams believe architecture is something you document once and then “enforce by convention”. You agree on layers, naming rules, dependency directions, and CDI usage, and you assume code reviews will keep things in line. This works for a while.
Then the team grows. Deadlines get tighter. Someone adds a shortcut “just this once”. A REST resource talks directly to a repository. A service leaks a framework class into the API. A test sneaks production-only dependencies onto the classpath. Nothing breaks immediately, so nobody notices.
Six months later, the codebase feels heavy. Refactoring becomes risky. You hesitate to move packages because you don’t know what depends on what. At two in the morning, when a change causes a cascade of failures, you realize the real problem: your architecture only exists in people’s heads.
Architecture tests fix this by turning architectural decisions into executable rules. If someone breaks the rules, the build fails. Not in production. Not in review. Immediately.
In this tutorial, we’ll build exactly that for a Quarkus application using Taikai, a rule-based architecture testing library built on top of ArchUnit. The goal is not academic purity. The goal is to keep your system understandable and safe as it evolves.
I have written about ArchUnit and BCE pattern before. Make sure to check out that post too:
Guard Your Code: Enforcing Architecture Boundaries in Quarkus with ArchUnit
Architecture is a constraint. Good constraints free teams to move fast without breaking the system. This tutorial shows how to codify architectural boundaries in a Quarkus app using ArchUnit, borrowing Boundary–Control–Entity (BCE) ideas popularized by Adam Bien. We’ll build a tiny “orders” service, express a few pragmatic rules, and enforce them with t…
What You’ll Learn
• Setting up a Quarkus project with Taikai
• Writing architecture tests to enforce coding standards
• Testing Java conventions, naming patterns, and import rules
• Enforcing Quarkus-specific architectural patterns
• Creating custom architecture rules
Prerequisites
You’ll need a standard Java development setup and basic familiarity with Quarkus testing.
Java 17 or newer
Maven 3.8+
Basic Quarkus knowledge
JUnit 5
Project Setup
Create a Quarkus Project
We start with a simple Quarkus REST application. Nothing fancy. The point is to have enough structure to enforce rules. And you don’t have to follow along, you can just grab the full project from my Github repository.
Create the project:
mvn io.quarkus:quarkus-maven-plugin:create \
-DprojectGroupId=com.example.architecture \
-DprojectArtifactId=quarkus-taikai-demo \
-Dextensions="quarkus-rest" \
-DclassName="com.example.architecture.GreetingResource" \
-Dpath="/hello"
cd quarkus-taikai-demoNow add Taikai as a test-only dependency. Architecture rules are tests. They should never leak into production.
<dependency>
<groupId>com.enofex</groupId>
<artifactId>taikai</artifactId>
<version>1.49.0</version>
<scope>test</scope>
</dependency>Create Sample Application Structure
Let’s create a more comprehensive project structure to test. Create the following packages and classes:
Creating a Minimal Layered Structure
We need something to validate, so we create a simple layered setup: resource, service, repository.
Service layer
Create src/main/java/com/example/architecture/service/GreetingService.java:
package com.example.architecture.service;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class GreetingService {
public String greet(String name) {
return "Hello, " + name + "!";
}
}This is a CDI-managed service. No framework leakage. No REST concerns.
Repository layer
Create src/main/java/com/example/architecture/repository/UserRepository.java:
package com.example.architecture.repository;
import java.util.ArrayList;
import java.util.List;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class UserRepository {
private final List<String> users = new ArrayList<>();
public void addUser(String user) {
users.add(user);
}
public List<String> getAllUsers() {
return new ArrayList<>(users);
}
}Again, very simple. The important part is the package and role, not the logic.
REST resource
Update and move(!) the src/main/java/com/example/architecture/resource/GreetingResource.java:
package com.example.architecture.resource;
import com.example.architecture.service.GreetingService;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/hello")
public class GreetingResource {
@Inject
GreetingService greetingService;
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return greetingService.greet("Quarkus");
}
@GET
@Path("/{name}")
@Produces(MediaType.TEXT_PLAIN)
public String greet(@PathParam("name") String name) {
return greetingService.greet(name);
}
}At this point, everything works. And this is exactly when architecture drift usually starts.
Basic Architecture Tests
There is no runtime configuration needed for Taikai. It runs as part of your test suite. The only “configuration” is the rules you write.
That’s intentional. Architecture decisions should be explicit, versioned, and reviewed like code.
Before we add our architecture tests, let’s make sure we “fix” the functional test. Replace the src/main/test/java/com/example/architecture/GreetingResourceTest.java with the following:
package com.example.architecture;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
@QuarkusTest
class GreetingResourceTest {
@Test
void testHelloEndpoint() {
given()
.when().get("/hello")
.then()
.statusCode(200)
.body(is("Hello, Quarkus!"));
}
}Create Your First Architecture Test
Create a new test class: src/main/test/java/com/example/architecture/ArchitectureTest.java:
package com.example.architecture;
import org.junit.jupiter.api.Test;
import com.enofex.taikai.Taikai;
class ArchitectureTest {
@Test
void shouldFollowBasicJavaRules() {
Taikai.builder()
.namespace("com.example.architecture")
.java(java -> java
.noUsageOfDeprecatedAPIs()
.methodsShouldNotDeclareGenericExceptions())
.build()
.check();
}
}This already gives you value. It prevents two common long-term problems: deprecated APIs and lazy exception handling.
Run the tests:
./mvnw testIf someone adds throws Exception or uses a deprecated API, the build fails. No discussion. No “we’ll fix it later”.
Enforcing Naming Conventions
Naming is architecture. Inconsistent naming kills readability.
@Test
void shouldFollowNamingConventions() {
Taikai.builder()
.namespace("com.example.architecture")
.java(java -> java
.naming(naming -> naming
.classesShouldNotMatch(".*Impl")
.interfacesShouldNotHavePrefixI()
.constantsShouldFollowConventions()
)
)
.build()
.check();
}What this test checks:
• classesShouldNotMatch(”.*Impl”) - Classes should not end with ‘Impl’
• interfacesShouldNotHavePrefixI() - Interfaces should not start with ‘I’
• constantsShouldFollowConventions() - Constants should be UPPER_SNAKE_CASE
This prevents the classic “ServiceImpl”, “IService”, and random constant styles that creep in over time.
Import and Dependency Rules
Imports tell the real dependency story. This is where architecture usually breaks first.
@Test
void shouldHaveCleanImports() {
Taikai.builder()
.namespace("com.example.architecture")
.java(java -> java
.imports(imports -> imports
.shouldHaveNoCycles()
.shouldNotImport("..internal..")
.shouldNotImport("org.junit..")
)
)
.build()
.check();
}This stops cyclic dependencies and prevents test-only APIs from leaking into production code.
Enforcing Layer Boundaries
Now the important part. We encode the layered architecture directly.
@Test
void shouldFollowLayeredArchitecture() {
Taikai.builder()
.namespace("com.example.architecture")
.java(java -> java
.classesShouldResideInPackage(".*Service", "..service..")
.classesShouldResideInPackage(".*Repository", "..repository..")
)
.build()
.check();
}This means a *Service class in the wrong package is not “ugly”. It’s illegal.
CDI and Quarkus-Specific Rules
Test Dependency Injection Patterns
Ensure proper dependency injection practices in Quarkus
@Test
void shouldUseConstructorInjection() {
Taikai.builder()
.namespace("com.example.architecture")
.java(java -> java
.fieldsShouldNotBePublic()
)
.build()
.check();
}Note: While Quarkus supports field injection with @Inject, constructor injection is considered a best practice for testability and immutability.
Test CDI Bean Scopes
In Quarkus, CDI scope mistakes show up late. We catch them early.
import jakarta.enterprise.context.ApplicationScoped;
@Test
void servicesShouldBeApplicationScoped() {
Taikai.builder()
.namespace("com.example.architecture")
.java(java -> java
.classesShouldBeAnnotatedWith(".*Service", ApplicationScoped.class)
)
.build()
.check();
}If someone forgets a scope, the build fails. No guessing at runtime.
Custom Architecture Rules
Create Custom Rules with ArchUnit
Sometimes you need rules specific to your project. Here’s how to create custom rules:
@Test
@DisplayName("Resources should only depend on services")
void resourcesShouldOnlyDependOnServices() {
JavaClasses classes = new ClassFileImporter()
.importPackages("com.example.architecture");
ArchRule rule = classes()
.that().haveNameMatching(".*Resource")
.should().onlyDependOnClassesThat()
.haveNameMatching(".*Service|java..*|jakarta..*");
rule.check(classes);
}
Layer Dependency Rules
Enforce strict layer dependencies:
import static com.tngtech.archunit.library.Architectures.layeredArchitecture;
@Test
@DisplayName("Should respect layered architecture")
void shouldRespectLayeredArchitecture() {
JavaClasses classes = new ClassFileImporter()
.importPackages("com.example.architecture");
layeredArchitecture()
.consideringAllDependencies()
.layer("Resource").definedBy("..resource..")
.layer("Service").definedBy("..service..")
.layer("Repository").definedBy("..repository..")
.whereLayer("Resource").mayNotBeAccessedByAnyLayer()
.whereLayer("Service").mayOnlyBeAccessedByLayers("Resource")
.whereLayer("Repository").mayOnlyBeAccessedByLayers("Service")
.check(classes);
}Best Practices and Tips
Organizing Your Tests
Separate concerns: Create different test methods for different architectural aspects
Use descriptive names: Test method names should clearly describe what they validate
Fail fast: Architecture tests should run early in your build pipeline
Performance Considerations
Architecture tests can be slow on large codebases
Consider caching the JavaClasses object for multiple rules
Use specific package filters to reduce analysis scope
Common Pitfalls to Avoid
Over-constraining: Don’t create rules that are too strict and hinder productivity
Ignoring violations: Treat architecture test failures as serious as functional test failures
No documentation: Document why each architectural rule exists
Conclusion
You’ve now learned how to implement comprehensive architecture testing in Quarkus applications using Taikai. These tests will help you maintain clean architecture, enforce coding standards, and catch architectural violations early in development.
Next Steps
Explore additional Taikai rules for Spring, logging, and testing frameworks
Create custom architecture rules specific to your domain
Integrate architecture testing into your team’s development workflow
Share architectural rules across multiple projects using rule profiles
Additional Resources
• Taikai GitHub: https://github.com/enofex/taikai
• Taikai Documentation: https://enofex.github.io/taikai/
• Quarkus Testing Guide: https://quarkus.io/guides/getting-started-testing
• ArchUnit Documentation: https://www.archunit.org/
More ways to prevent architecture drift?
Your Quarkus Architecture Is Drifting. JQAssistant Can Prove It.
I ran into JQAssistant the first time on a large Java system where the architecture looked clean on slides but not in the code.




