Services and layers
Over the previous lessons the blog grew three kinds of classes: a controller accepts requests, a service solves the task, a repository stores data. This split is called layers. Each layer knows only about the one below it, which lets you change one part without breaking the others: in the next chapter we replace the in-memory store with a database, and the controller will not even notice.
Three layers
| Layer | Annotation | Responsible for | Must not |
|---|---|---|---|
| Controller | @RestController | Addresses, reading the request, response codes | Hold domain rules |
| Service | @Service | Logic: checks, calculations, transactions | Know about HTTP and ResponseEntity |
| Repository | @Repository | Reading and writing data | Decide what may be saved |
A request goes top down: controller → service → repository. The response goes back.
The whole blog service
// src/main/java/com/example/blog/PostService.java
@Service
public class PostService {
private final PostRepository repository;
private final Clock clock;
private final BlogProperties properties;
public PostService(PostRepository repository, Clock clock, BlogProperties properties) {
this.repository = repository;
this.clock = clock;
this.properties = properties;
}
public List<Post> findAll(String author) {
return repository.findAll().stream()
.filter(post -> author == null || post.author().equals(author))
.sorted(Comparator.comparing(Post::id))
.limit(properties.pageSize())
.toList();
}
public Post findById(Long id) {
return repository.findById(id)
.orElseThrow(() -> new PostNotFoundException(id));
}
public Post create(NewPostRequest request) {
Post post = new Post(null, request.title(), request.body(), request.author(), Instant.now(clock));
return repository.save(post);
}
public Post update(Long id, NewPostRequest request) {
Post old = findById(id);
return repository.save(new Post(id, request.title(), request.body(), request.author(), old.createdAt()));
}
public void delete(Long id) {
findById(id);
repository.deleteById(id);
}
}Note that the service throws PostNotFoundException instead of returning a 404 code. It does not know it is called over HTTP. Turning the exception into a response is the job of the handler from the previous lesson.
Project files
src/main/java/com/example/blog/
├── BlogApplication.java
├── AppConfig.java settings and beans
├── BlogProperties.java a group of settings
├── PostController.java controller layer
├── ApiExceptionHandler.java errors → responses
├── NewPostRequest.java input data
├── PostService.java service layer
├── PostNotFoundException.java
├── PostRepository.java data layer
└── Post.java the modelWhile there are few classes, one package is fine. As the project grows, classes are split into feature packages: com.example.blog.post, com.example.blog.comment, com.example.blog.user. Everything about posts sits together, with no jumping between controllers and services folders.
Checking everything at once
curl -X POST http://localhost:8080/api/posts -H "Content-Type: application/json" \
-d '{"title":"Second","body":"Text","author":"oleg"}'
curl -X PUT http://localhost:8080/api/posts/2 -H "Content-Type: application/json" \
-d '{"title":"Second (edited)","body":"Text","author":"oleg"}'
curl -i -X DELETE http://localhost:8080/api/posts/2Creating returns 201, updating returns 200 with the new title, deleting returns 204. Deleting the same post again answers 404 through the error handler.
Common mistake
It is tempting to pass the repository straight into the controller and write the logic there: "find, check, save". It is shorter at first, but later the same logic is needed from a site page or a background job — and has to be copied. Keep the controller thin: take the request, call the service, return the response.