← Back to Java series
🌱
Spring Boot
Spring Boot Β· Prerequisite: previous lecture

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.

Spring BootJavaserviceDIDTO
Duration
⏱ ~1.5-2 hours
Level
πŸ“Š Intermediate-Advanced
Prerequisite
🎯 Previous lecture or equivalent knowledge
OUTCOME
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

text
[Client]
   ↓ HTTP
[@RestController]        ←→ HTTP, request validation, status codes
   ↓ DTO
[@Service]               ←→ business logic, transactions
   ↓ Entity
[@Repository]            ←→ data access (DB, in-memory store)

2) Annotations

AnnotationRole
`@Component`Generic bean (most general)
`@Service`Business logic
`@Repository`Data access
`@Controller`Web controller (returns view names)
`@RestController``@Controller` + `@ResponseBody`

3) Constructor injection (preferred)

java
@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
java
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`

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)

java
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`

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)

java
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

  1. Putting business logic in the controller β€” it becomes hard to test and reuse
  2. Returning the entity directly to the client β€” leaks fields, ties API to DB schema
  3. Using `@Autowired` on a field instead of the constructor
  4. Catching exceptions in the controller β€” let global exception handlers do it
  5. 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

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

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

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

bash
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.

Example code / lecture materials

All lecture materials and example code are openly available on GitHub.

View on GitHub β†—