Designing the Architecture
Designing the Architecture
Every non-trivial Java application eventually reaches a point where dumping everything into a single class — or even a flat collection of classes — becomes painful. Code changes ripple unexpectedly, tests become impossible to write in isolation, and onboarding a new teammate means reading the entire codebase before touching anything. A deliberate architecture prevents all of this.
In this lesson you will design the layered architecture for the capstone application, learn why each layer exists, understand what belongs in each one, and organise everything into a package structure that enforces those boundaries by convention and, optionally, by tooling.
The Classic Three-Layer Model
Most business applications are well-served by three horizontal layers stacked on top of each other:
- Presentation / Interface layer — receives input and delivers output. In a CLI app this is argument parsing and console output. In a REST service this is HTTP controllers and JSON serialisation.
- Business / Domain layer — contains the rules that make the application valuable. This layer knows nothing about how data arrives or where it goes.
- Data / Infrastructure layer — persists and retrieves data. In our capstone this will be JDBC or an in-memory store; in a production system it could be JPA, an external API, a message broker, and so on.
Translating Layers to Java Packages
Packages are Java's primary mechanism for expressing architectural intent. A well-chosen package layout makes the architecture visible to every reader of the codebase, and most static analysis tools (ArchUnit, Checkstyle, SpotBugs) can enforce package-level rules in CI.
For the capstone — a small task-management CLI — a practical layout looks like this:
TaskRepository lives in repository/ because it is the contract; InMemoryTaskRepository (or later a JdbcTaskRepository) is an implementation detail that satisfies that contract. Swapping implementations requires zero changes in the service layer.
Why Separate the Model Package?
The model package holds plain Java objects — records or classes with no framework annotations, no SQL, no HTTP — that represent the concepts the application reasons about. All layers reference model objects freely, so placing the model inside any single layer would create an artificial dependency. It stands alone.
The Repository Interface — Abstracting Persistence
The business layer never calls JDBC or touches a file directly. Instead it talks to a repository interface defined in terms of domain objects. This is the Repository pattern from Domain-Driven Design, and it is one of the highest-leverage patterns in enterprise Java.
The implementation — in-memory for now — lives in the same package but is a detail:
The Service Layer — Where Business Logic Lives
The service layer receives a TaskRepository via its constructor (constructor injection — no framework required). It orchestrates domain rules and delegates persistence. It does not format output and it does not care about HTTP or CLI specifics.
dueDate is before today and status is not DONE" is a business rule; the repository should expose findAll() and the service applies the filter with a stream.
Trade-offs and Alternatives
The three-layer model is a pragmatic starting point, not a dogma. As you design, be aware of:
- Hexagonal Architecture (Ports & Adapters): Makes the inward-dependency rule explicit by naming the boundary — the port (interface) — and the adapter (implementation). Scales better for multiple I/O channels (CLI + REST + event-driven).
- Vertical slicing: Instead of horizontal layers, group by feature (
task/,user/). Each feature owns its own service, repository, and model. Reduces coupling between features but can duplicate infrastructure code. - Anemic vs. rich domain model: Our
Taskrecord is mostly data; the service holds logic. A richer model would embed state-transition methods (task.complete()) directly in the domain object. Both are valid — the record approach is simpler; the rich model scales better as rules grow.
Summary
A layered architecture separates what the user sees (UI), what the application knows (service), and where data lives (repository). Java packages make this structure visible and tooling-enforceable. The dependency rule — always point inward toward the domain — is the single most important constraint to uphold. With this skeleton in place, the next lesson will flesh out the domain model in detail.