Repository, Service & DTO
Beyond the GoF catalogue, Spring Boot applications are shaped by the enterprise layering patterns popularised by Martin Fowler’s Patterns of Enterprise Application Architecture — chiefly Repository, Service Layer, and DTO/Assembler. Together they enforce separation of concerns: each layer has one job and talks only to its neighbour. Spring gives each pattern a first-class home: @Repository, @Service, and @RestController.
The classic three layers
A request flows downward and a response flows back up, with each layer translating between representations:
HTTP ─▶ Controller (web) ── DTOs ──▶ Service (business) ── entities ──▶ Repository (data)
│
Database
| Layer | Stereotype | Responsibility | Knows about |
|---|---|---|---|
| Web | @RestController | HTTP, validation, DTO mapping | DTOs, the service interface |
| Business | @Service | Use cases, transactions, rules | Entities, repositories |
| Data | @Repository | Persistence, queries | Entities, the database |
Repository pattern
The Repository pattern hides data-access mechanics behind a collection-like interface, so the rest of the app treats persistence as “a collection of domain objects.” Spring Data takes this further: you declare an interface and the framework generates the implementation.
import org.springframework.data.jpa.repository.JpaRepository;
public interface ProductRepository extends JpaRepository<Product, Long> {
List<Product> findByCategory(String category); // query derived from the name
}
No implementation class exists in your source — Spring Data builds a proxy at runtime. See repositories for derived queries, @Query, and pagination.
Service layer pattern
The Service Layer defines the application’s use cases and is the natural home for transaction boundaries and business rules. It depends on repositories, never the other way around.
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductRepository repository;
private final ProductMapper mapper;
@Transactional(readOnly = true)
public ProductDto findById(Long id) {
Product entity = repository.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id));
return mapper.toDto(entity); // entity -> DTO at the boundary
}
@Transactional
public ProductDto create(CreateProductRequest request) {
Product saved = repository.save(mapper.toEntity(request));
return mapper.toDto(saved);
}
}
Keeping @Transactional on the service (not the controller or repository) is the convention — a use case is the unit of work.
DTO and Assembler pattern
A DTO (Data Transfer Object) is a flat, serialisable object that crosses a boundary — typically the HTTP layer. The Assembler (mapper) translates between entities and DTOs. This keeps your JPA entities out of your API contract, so internal schema changes do not leak to clients and lazy-loading proxies never get serialised by accident.
public record ProductDto(Long id, String name, BigDecimal price) { }
public record CreateProductRequest(
@NotBlank String name,
@Positive BigDecimal price) { }
import org.springframework.stereotype.Component;
@Component
public class ProductMapper {
public ProductDto toDto(Product e) {
return new ProductDto(e.getId(), e.getName(), e.getPrice());
}
public Product toEntity(CreateProductRequest r) {
return new Product(r.name(), r.price());
}
}
The controller then deals only in DTOs:
@RestController
@RequestMapping("/api/products")
@RequiredArgsConstructor
public class ProductController {
private final ProductService service;
@GetMapping("/{id}")
public ProductDto get(@PathVariable Long id) {
return service.findById(id);
}
}
See DTO pattern for mapping options including MapStruct.
Warning: Never return JPA entities directly from a controller. Serializing an entity can trigger lazy-loading exceptions, expose internal fields, and tightly couple your API to your database schema. Map to a DTO at the service boundary.
Package-by-layer vs package-by-feature
How you arrange these layers into packages matters as the codebase grows.
| Package-by-layer | Package-by-feature | |
|---|---|---|
| Structure | controller/, service/, repository/ | product/, order/, payment/ |
| A change to one feature | Touches many packages | Stays in one package |
| Discoverability | Layer-first | Domain-first |
| Scales to large apps | Poorly | Well |
package-by-layer package-by-feature
└─ controller/ └─ product/
├─ ProductController ├─ ProductController
└─ OrderController ├─ ProductService
└─ service/ ├─ ProductRepository
├─ ProductService └─ ProductDto
└─ OrderService └─ order/
└─ repository/ ├─ OrderController
├─ ProductRepository └─ ...
└─ OrderRepository
Tip: Package-by-feature is the recommended default for non-trivial applications: a feature lives in one place, dependencies between features are visible, and packages can be made
package-privateto enforce boundaries. See best practices.
These layering patterns are not decoration — they are what keep a Spring Boot codebase testable (mock the layer below), changeable (swap an implementation), and understandable (each class has one reason to change).