25. Services and Dependency Injection
For small examples one controller class is fine, but as a project grows **separating concerns** is crucial for maintainability. This lecture covers Spring's conventional 3-layer architecture and the **DTO** pattern for safe data transfer between layers.
What you'll learn
- 1Distinguish Controller / Service / Repository
- 2Use the `@Service` and `@Repository` annotations
- 3Apply the **constructor injection** pattern
- 4Distinguish DTOs from domain objects
- 5Apply the principle "business logic belongs in the service"
Overview
For small examples one controller class is fine, but as a project grows **separating concerns** is crucial for maintainability. This lecture covers Spring's conventional 3-layer architecture and the **DTO** pattern for safe data transfer between layers.
Core Concepts
1) 3-layer architecture
[Client]
β HTTP
[@RestController] ββ HTTP, request validation, status codes
β DTO
[@Service] ββ business logic, transactions
β Entity
[@Repository] ββ data access (DB, in-memory store)2) Annotations
| Annotation | Role |
|---|---|
| `@Component` | Generic bean (most general) |
| `@Service` | Business logic |
| `@Repository` | Data access |
| `@Controller` | Web controller (returns view names) |
| `@RestController` | `@Controller` + `@ResponseBody` |
3) Constructor injection (preferred)
@Service
public class UserService {
private final UserRepository repo;
public UserService(UserRepository repo) { this.repo = repo; }
public User create(CreateUserRequest req) { return repo.save(new User(req.name(), req.age())); }
}- Required dependencies are explicit
- Fields can be `final` β thread-safe
- Easy to test (pass a stub in the constructor)
4) DTO vs domain object
- **Domain object / entity** β the model the service and repo operate on
- **DTO (Data Transfer Object)** β the shape exchanged with the outside world
- Map between them in the controller / service
record CreateUserRequest(String name, int age) {} // inbound DTO
record UserResponse(Long id, String name) {} // outbound DTO
class User { // domain entity
Long id; String name; int age;
// ...
}Examples
Example 1 β `UserService.java`
package com.example.demo;
import org.springframework.stereotype.Service;
@Service
public class UserService {
private final UserRepository repo;
public UserService(UserRepository repo) {
this.repo = repo;
}
public User create(String name, int age) {
if (name == null || name.isBlank()) throw new IllegalArgumentException("name required");
if (age < 0) throw new IllegalArgumentException("age >= 0");
return repo.save(new User(name, age));
}
public java.util.List<User> all() {
return repo.findAll();
}
}Example 2 β `UserRepository.java` (in-memory)
package com.example.demo;
import java.util.*;
import org.springframework.stereotype.Repository;
@Repository
public class UserRepository {
private final Map<Long, User> data = new LinkedHashMap<>();
private long seq = 0;
public User save(User u) {
u.id = ++seq;
data.put(u.id, u);
return u;
}
public List<User> findAll() { return new ArrayList<>(data.values()); }
public Optional<User> findById(Long id) { return Optional.ofNullable(data.get(id)); }
}Example 3 β `UserController.java`
package com.example.demo;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService service;
public UserController(UserService service) { this.service = service; }
record CreateUserRequest(String name, int age) {}
record UserResponse(Long id, String name, int age) {}
@PostMapping
public ResponseEntity<UserResponse> create(@RequestBody CreateUserRequest req) {
User u = service.create(req.name(), req.age());
return ResponseEntity.status(HttpStatus.CREATED)
.body(new UserResponse(u.id, u.name, u.age));
}
@GetMapping
public java.util.List<UserResponse> all() {
return service.all().stream()
.map(u -> new UserResponse(u.id, u.name, u.age))
.toList();
}
}Example 4 β `User.java` (domain entity)
package com.example.demo;
public class User {
Long id;
String name;
int age;
public User(String name, int age) { this.name = name; this.age = age; }
}Common Mistakes
- Putting business logic in the controller β it becomes hard to test and reuse
- Returning the entity directly to the client β leaks fields, ties API to DB schema
- Using `@Autowired` on a field instead of the constructor
- Catching exceptions in the controller β let global exception handlers do it
- Reaching across layers (Controller β Repository) and skipping the Service
Summary
- Controller = web concerns, Service = business logic, Repository = data
- Constructor injection makes dependencies explicit
- DTOs decouple your API from your domain model
Practice
# Practice - 25. Services and DI
## Exercise 1 β Move logic into a service
- Take the `BookController` from lecture 24, move the `Map` into a `BookRepository` (`@Repository`).
- Add a `BookService` (`@Service`) that the controller calls.
## Exercise 2 β DTOs
- Create `CreateBookRequest` and `BookResponse` records, map between them in the controller.
## Solutions After trying it yourself, compare with [`answer/`](./answer/).
Solution code (homework/answer/)
answer/BookRepository.java
package com.example.demo;
import java.util.*;
import org.springframework.stereotype.Repository;
@Repository
public class BookRepository {
private final Map<Long, String> data = new LinkedHashMap<>();
private long seq = 0;
public Long save(String title) { long id = ++seq; data.put(id, title); return id; }
public Map<Long, String> all() { return data; }
public boolean remove(Long id) { return data.remove(id) != null; }
}
answer/BookService.java
package com.example.demo;
import java.util.*;
import org.springframework.stereotype.Service;
@Service
public class BookService {
private final BookRepository repo;
public BookService(BookRepository repo) { this.repo = repo; }
public Long create(String title) {
if (title == null || title.isBlank()) throw new IllegalArgumentException("title required");
return repo.save(title);
}
public Map<Long, String> all() { return repo.all(); }
public boolean delete(Long id) { return repo.remove(id); }
}
answer/BookController.java
package com.example.demo;
import java.util.*;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/books")
public class BookController {
private final BookService service;
public BookController(BookService service) { this.service = service; }
record CreateBookRequest(String title) {}
record BookResponse(Long id, String title) {}
@PostMapping
public ResponseEntity<BookResponse> create(@RequestBody CreateBookRequest req) {
Long id = service.create(req.title());
return ResponseEntity.status(HttpStatus.CREATED).body(new BookResponse(id, req.title()));
}
@GetMapping
public List<BookResponse> all() {
return service.all().entrySet().stream()
.map(e -> new BookResponse(e.getKey(), e.getValue()))
.toList();
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
return service.delete(id) ? ResponseEntity.noContent().build() : ResponseEntity.notFound().build();
}
}
Try It Yourself
mvn spring-boot:run
curl -X POST localhost:8080/users -H 'Content-Type: application/json' -d '{"name":"Jisoo","age":21}'Next Lecture
[26_JPA](../26_λ°μ΄ν°_μ μ₯/) β persist data with Spring Data JPA and H2.
All lecture materials and example code are openly available on GitHub.
View on GitHub β