Skip to content

Repository / Service / Controller

Spring Boot uses a layered architecture built on OOP principles. Each layer has one responsibility and only talks to the layer directly below it:

Controller  →  Service  →  Repository  →  Database

Repository

A repository is an interface for accessing data from a database. It extends JpaRepository, which gives you a set of CRUD methods for free:

Method Description
findAll() Returns all records
findById(id) Returns an Optional by primary key
save(entity) Inserts or updates a record
deleteById(id) Deletes a record by primary key
existsById(id) Returns true if the record exists

You can also define custom queries just by following JPA's naming convention — no SQL needed. JPA reads the method name and generates the query automatically:

Optional<User> findByFirstname(String firstname);
// → SELECT * FROM users WHERE firstname = ?

List<User> findByAgeGreaterThan(int age);
// → SELECT * FROM users WHERE age > ?

@Repository tells Spring Boot to manage this bean, though it's technically optional since Spring detects any interface extending JpaRepository. It's still good practice to keep it.

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByFirstname(String firstname);
}

Service

A service holds the business logic. Controllers call it, and it calls the repository. This is where you add validations, transformations, or any processing before reading/writing data.

The repository is injected via constructor injection (preferred over @Autowired on a field) because:

  • Dependencies are explicit and required — no hidden state
  • Fields can be final, making the class immutable
  • Easier to unit test — you can pass a mock directly through the constructor
@Service
public class UserService {
    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User updateUser(User modifiedUser) {
        return userRepository.findById(modifiedUser.getId())
            .map(existingUser -> {
                existingUser.setFirstname(modifiedUser.getFirstname());
                // ...
                return userRepository.save(existingUser);
            })
            .orElseThrow(() -> new RuntimeException("User not found"));
    }
}

Controller

A controller has no business logic — it only receives HTTP requests and delegates to the right service method.

  • @RestController is a shortcut for @Controller + @ResponseBody, meaning every method returns JSON directly.
  • @RequestMapping("/api/users") sets the base path for all endpoints in the class.
  • HTTP methods are mapped with @GetMapping, @PostMapping, @PutMapping, @DeleteMapping, etc.
  • ResponseEntity<T> lets you control the full HTTP response: status code, headers, and body.

Spring Boot automatically handles constructor injection when there is only one constructor, so no @Autowired is needed.

@RestController
@RequestMapping("/api/users")
public class UserController {
    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/{id}")
    public ResponseEntity<User> getUserById(@PathVariable Long id) {
        return userService.getUserById(id)
            .map(ResponseEntity::ok)
            .orElseGet(() -> ResponseEntity.notFound().build());
    }

    @PostMapping
    public ResponseEntity<User> createUser(@RequestBody User user) {
        return new ResponseEntity<>(userService.createUser(user), HttpStatus.CREATED);
    }
}

@PathVariable binds {id} from the URL to the method parameter. @RequestBody deserializes the JSON request body into a Java object.