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:
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.
@RestControlleris 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);
}
}
@PathVariablebinds{id}from the URL to the method parameter.@RequestBodydeserializes the JSON request body into a Java object.