exit lab
Backend Engineering
Level 4·spring·system·beginner

Layered Spring Boot Architecture

Place HTTP mapping, validation, business orchestration, transactions, MyBatis SQL, and models in explicit layers with inward dependency direction.

45 minutes Java 21, Spring Boot 3.5.16, Spring Framework 6.2, MyBatis starter 3.x Updated 2026-07-16
Java 21Spring Boot 3.5Spring MVCMyBatis

Learning objectives

  • Assign one clear responsibility to each application layer
  • Keep transport and persistence models from leaking into business policy

What it is

Layered architecture separates an HTTP adapter, application use cases, and persistence details. In this curriculum the practical flow is Controller → Service → Mapper → SQL → Database, with mapping at boundaries and dependencies pointing toward business policy.

Why it matters

Clear boundaries make a Spring application easier to test, review, change, and operate. They also prevent framework input, database columns, and SQL behavior from becoming the public API or the place where business decisions hide.

How it works

Primary dependency flow

Controller
Service
Mapper interface
Mapper XML / SQL
PostgreSQL
Text alternative: The controller depends on a service, the service depends on a mapper interface, the mapper binds SQL from XML, and SQL accesses PostgreSQL. Results return in the opposite direction.
LayerOwnsDoes not own
ControllerHTTP input/output, status, validation triggerBusiness rules or SQL
ServiceUse case, authorization context, orchestration, transaction boundaryServlet objects or XML mapping
MapperTyped persistence operationsHTTP responses or cross-use-case policy
SQLSet operations, joins, locking, affected-row semanticsPresentation shape
DatabaseConstraints, durability, isolation, indexingClient-specific error copy

Simple example

package-structure.txt
com.henheang.task
├── api/
│   ├── TaskController.java
│   ├── CreateTaskRequest.java
│   └── TaskResponse.java
├── application/
│   └── TaskService.java
├── domain/
│   ├── Task.java
│   └── TaskStatus.java
├── persistence/
│   ├── TaskMapper.java
│   └── TaskRow.java
└── support/
    └── ApiExceptionHandler.java

resources/mappers/TaskMapper.xml

Package by feature first when it keeps related code navigable; use subpackages to make layer boundaries visible. A giant global controller/service/repository package tree becomes harder to own as features grow.

Backend example

TaskService.java
@Service
public class TaskService {
    private final TaskMapper taskMapper;

    public TaskService(TaskMapper taskMapper) {
        this.taskMapper = taskMapper;
    }

    @Transactional
    public TaskResponse create(CreateTaskCommand command, long actorId) {
        Task task = Task.create(command.title(), command.dueAt(), actorId);
        TaskRow row = TaskRow.from(task);
        taskMapper.insert(row);
        return TaskResponse.from(row);
    }
}

Production example

  1. 1

    HTTP

    Controller accepts a validated request and authenticated principal, calls one use case, and maps the result to HTTP.

  2. 2

    Policy

    Service enforces ownership and state transition, then defines the atomic transaction boundary.

  3. 3

    Persistence

    Mapper executes parameterized SQL and returns affected rows or typed results; database constraints arbitrate concurrent validity.

  4. 4

    Failure

    Exceptions cross into a global handler that maps known categories to stable RFC 9457 responses and logs unexpected causes once.

Common mistakes

  • Putting business rules and mapper calls directly in controllers
  • Returning mapper row objects as the API contract
  • Using field injection
  • Opening transactions in controllers
  • Catching every exception in every layer
  • Adding pass-through layers with no boundary or ownership value

Best practices

  • Use constructor injection and final dependencies
  • Keep controllers thin and services use-case focused
  • Validate syntax at the edge and business rules in the service/domain
  • Keep SQL explicit and reviewable
  • Map expected failures once at the API boundary
  • Test layers according to their real responsibility

Trade-offs

DesignAdvantageCost
Separate DTO/domain/row modelsStrong compatibility and persistence boundariesMapping code
Shared simple modelLess code for a small stable featureTransport and schema changes become coupled
Feature packagesOwnership and navigationCross-cutting conventions need discipline
Global layer packagesObvious layer inventoryFeatures scatter across the codebase

Interview questions

  • What belongs in a controller versus a service?
  • Why prefer constructor injection?
  • Should a DTO be the same as a database row?
  • Where should authorization and transactions live?
  • When is an extra interface useful rather than ceremonial?

Hands-on task

  1. 1

    Sketch

    Draw the Task create and list flows with layer inputs, outputs, and failure types.

  2. 2

    Implement

    Build controller, service, mapper, XML, schema, DTO mapping, and global error handling.

  3. 3

    Review

    Write one sentence for each class responsibility; split or remove any class that cannot be explained clearly.

References

  • Spring: Spring Boot 3.5.16 System Requirements — https://docs.spring.io/spring-boot/3.5/system-requirements.html
  • Spring: Dependency Injection — https://docs.spring.io/spring-framework/reference/core/beans/dependencies/factory-collaborators.html
  • Spring: Annotated Controllers — https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-controller.html
  • MyBatis: MyBatis Spring Boot Starter Reference — https://mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/

Retrieval check

Before you continue

  • Explain the main idea in your own words without rereading the page.
  • Name one production problem this knowledge helps prevent.
  • Describe where you would apply it in a Spring Boot project.

Source metadata

Primary references checked on 2026-07-16. Version scope is stated at the top of this page.