Spring Boot architecture patterns: constructor injection, Controller→Service→Repository layering, @Transactional on services, DTOs at layer boundaries, Bean Validation at the controller boundary. Use when designing, writing, or reviewing Spring Boot application structure.
Never inject dependencies via @Autowired fields or setter injection. Constructor injection makes dependencies explicit, supports immutability, and simplifies testing.
// Forbidden: field injection
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository; // hidden dependency, untestable without Spring
@Autowired
private CustomerRepository customerRepository;
}
// Correct: constructor injection
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final CustomerRepository customerRepository;
public OrderService(OrderRepository orderRepository, CustomerRepository customerRepository) {
this.orderRepository = orderRepository;
this.customerRepository = customerRepository;
}
}
With a single constructor, @Autowired is not needed — Spring injects automatically. With Lombok, @RequiredArgsConstructor on fields is acceptable.
finalHTTP Request → Controller → Service → Repository → Database
(maps HTTP) (owns logic) (owns data)
Controller responsibilities:
@TransactionalService responsibilities:
@Transactional)ResponseEntityRepository responsibilities:
@Transactional (service owns the transaction)// Correct layering
@RestController
@RequestMapping("/orders")
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping
public ResponseEntity<OrderResponse> createOrder(@RequestBody @Valid CreateOrderRequest request) {
OrderResponse response = orderService.createOrder(request);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
}
@Service
public class OrderService {
@Transactional
public OrderResponse createOrder(CreateOrderRequest request) {
Customer customer = customerRepository.findById(request.customerId())
.orElseThrow(() -> new CustomerNotFoundException(request.customerId()));
Order order = new Order(customer.getId(), request.items());
Order saved = orderRepository.save(order);
return OrderResponse.from(saved);
}
}
public interface OrderRepository extends JpaRepository<Order, UUID> {
List<Order> findByCustomerId(CustomerId customerId);
}
@Transactional on the Service LayerNever put @Transactional on a controller or repository method unless you have a specific, documented reason. The service is the transaction boundary.
// Wrong: transaction on controller
@RestController
public class OrderController {
@PostMapping("/orders")
@Transactional // wrong — controllers are not transaction owners
public ResponseEntity<OrderResponse> createOrder(...) { ... }
}
// Wrong: transaction on repository (Spring Data manages its own for basic CRUD)
public interface OrderRepository extends JpaRepository<Order, UUID> {
@Transactional // wrong — service should own the scope
List<Order> findByStatus(OrderStatus status);
}
// Correct: transaction on service
@Service
public class OrderService {
@Transactional
public OrderResponse createOrder(CreateOrderRequest request) { ... }
@Transactional(readOnly = true) // use readOnly for queries — performance hint to the DB
public List<OrderResponse> findByCustomer(CustomerId customerId) { ... }
}
Domain entities (@Entity classes) must not leave the service layer. Controllers receive request DTOs and return response DTOs. Entities are persistence models — leaking them to the HTTP layer couples your API shape to your database schema.
// Wrong: entity returned from controller
@GetMapping("/orders/{id}")
public Order getOrder(@PathVariable UUID id) { // @Entity returned directly
return orderService.findById(id);
}
// Correct: entity mapped to response DTO inside the service
@GetMapping("/orders/{id}")
public ResponseEntity<OrderResponse> getOrder(@PathVariable UUID id) {
OrderResponse response = orderService.findById(OrderId.of(id));
return ResponseEntity.ok(response);
}
// OrderResponse is a record — no JPA annotations, pure data
public record OrderResponse(String id, String customerId, String status, BigDecimal total) {
public static OrderResponse from(Order order) {
return new OrderResponse(
order.getId().toString(),
order.getCustomerId().value(),
order.getStatus().name(),
order.getTotal()
);
}
}
Validate incoming requests with Bean Validation (@Valid, @NotNull, @Size, etc.) in the controller. Services trust their inputs — they do domain validation (business rules), not structural validation.
// Request DTO with Bean Validation constraints
public record CreateOrderRequest(
@NotNull CustomerId customerId,
@NotEmpty @Size(max = 50) List<@Valid LineItemRequest> items
) {}
// Controller triggers validation
@PostMapping
public ResponseEntity<OrderResponse> createOrder(@RequestBody @Valid CreateOrderRequest request) {
return ResponseEntity.status(201).body(orderService.createOrder(request));
}
// Validation failures produce 400 automatically via @ControllerAdvice handling MethodArgumentNotValidException
@Autowired in TestsIn unit tests, inject manually via constructor. In integration tests, use @Autowired only in Spring-managed test classes (@SpringBootTest, @DataJpaTest).
// Unit test — no Spring, no @Autowired
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock private OrderRepository orderRepository;
@InjectMocks private OrderService orderService; // Mockito injects via constructor
}
// Integration test — Spring manages the context
@SpringBootTest
class OrderServiceIntegrationTest {
@Autowired private OrderService orderService; // correct here
}