← Back to the section · level 1 of 3

Almost every Spring project starts the same way: the controller calls a service, the service goes to a repository. This scheme is called a layered architecture. It's simple, clear, and in most cases works great — right up until the service grows to fifty methods.

What a layered architecture is

Three layers arranged in a chain:

HTTP request
    ↓
Controller  — accepts the request, calls the service, returns the response
    ↓
Service     — business logic, checks, orchestration
    ↓
Repository  — database queries

Each layer is responsible for its own thing. The controller doesn't touch the database — that's the repository's job. The repository doesn't accept HTTP — that's the controller's job. The service in the middle holds the logic.

The code looks roughly like this:

@RestController
class OrderController {
    private final OrderService service;

    @PostMapping("/orders")
    OrderResponse create(@RequestBody CreateOrderRequest req) {
        return service.createOrder(req);
    }
}

@Service
class OrderService {
    private final OrderRepository repo;

    @Transactional
    OrderResponse createOrder(CreateOrderRequest req) {
        // checks, logic, save
        Order order = new Order(req.getCustomerId(), req.getItems());
        repo.save(order);
        return OrderResponse.from(order);
    }
}

No special abstractions — just three classes and how they interact.

When this is enough

A layered architecture is a great fit for:

  • Real CRUD. Reference data, dictionaries, simple admin panels. The operations are create, read, update, delete. No complex logic inside.
  • A prototype or a short-lived service. If a service will live for a year or two and then be rewritten or shut down, investing in a complex structure makes no sense.
  • A small team with a well-understood domain. When the domain fits in your head in half an hour and there are almost no invariants, extra abstractions only get in the way.
  • A thin proxy. The service accepts a request and forwards it to another service or an external API — minimal business logic.

Level 1 is not an "unfinished" level. For some services it stays sufficient forever. Adding complexity for the sake of complexity is a mistake.

What you should have at this level

Thin controllers. The controller does exactly one thing: accepts the request, calls the service, returns the response. No logic inside the controller.

Services by subject area. OrderService, CustomerService — each responsible for its own topic. The logic and the repository calls live inside the service.

A transaction at the method level. One operation — one @Transactional method. Either everything is saved, or nothing is.

A single data model. What comes in from the API and what sits in the database are connected by a simple mapping. There are no separate domain models or value objects at this level — they show up higher.

Where it starts to get in the way

The layered model is good as long as the service stays small. Problems begin when it grows:

The service accumulates dozens of methods. An OrderService with thirty methods is no longer an obvious structure. It's unclear which of them are real business operations and which are helpers. You have to assemble the list of operations by hand, scanning the signatures.

One operation is called from several places with differences. If one operation is called from three controllers with slightly different checks, those checks drift apart over time — subtle bugs appear.

No single control point. Per-operation metrics, audit, a unified way of handling errors — all of it has to be added to every method by hand. Easy to miss, hard to maintain.

Hard to test an operation in isolation. A test of a business operation drags in the whole service class with all its dependencies. There's no way to run just one operation without the rest.

This isn't "bad code" — it's the limit of the layered model. It simply has no mechanism to explicitly single out individual operations.

What gets captured in the specification at this level

If the project uses a Use Case specification, at level 1 it is minimal: glossary, roles, data schema, and a list of operations. There are no aggregates or domain events — they are marked "not applicable at Level 1". The central entity is still moved into a separate file — the layout format is the same for all levels.

Signs it's time for level 2

  • The service classes have grown, and the operations in them can't be told apart.
  • You want automatic metrics, audit, and a unified way of handling errors for every operation.
  • A need has appeared to test one business operation in isolation, without running the whole service.

If even one of these is relevant, it's time for the next step: Level 2: Use Case Pattern. There each business operation is singled out into a separate UseCase + Handler.

In short

  • The layered architecture is three layers: Controller, Service, Repository.
  • It fits CRUD, prototypes, small services with a simple domain.
  • The controller is thin, the logic is in the service, the transaction is at the method level.
  • It breaks down when the service grows: the explicitness of operations is lost, metrics and audit don't scale.
  • Level 1 is a deliberate choice, not a half-measure. For some services it's sufficient forever.