WL
Java Full Stack Developer
Wassim Lagnaoui

Lesson 18: Spring Boot Application - All Components Working Together

Learn how to build a complete Spring Boot application with proper project structure, integrating controllers, services, repositories, security, and data access layers into a cohesive system.

Introduction

Building a complete Spring Boot application is like conducting an orchestra where every instrument must play in harmony to create beautiful music. You've learned individual components like controllers, services, repositories, security, and data access, but now it's time to see how they all work together in a real application. A well-structured Spring Boot application follows clear architectural patterns that separate concerns, promote maintainability, and enable scalability. Understanding how to organize your project structure, connect different layers, and manage dependencies between components is crucial for building professional applications. This lesson guides you through creating a complete application step by step, showing you how each piece fits together and demonstrating best practices for project organization that will serve you well in real-world development.


Project Structure

Definition

  • Project structure in Spring Boot follows conventions that organize code into logical packages based on functionality and architectural layers.
  • A well-organized structure separates presentation logic (controllers), business logic (services), data access logic (repositories), and configuration components.
  • This separation makes code easier to understand, test, and maintain while following Spring Boot's convention-over-configuration philosophy that reduces boilerplate and increases productivity.

Analogy

Think of project structure like organizing a well-run restaurant. The dining area (controllers) handles customer interactions, the kitchen (services) prepares the food according to recipes, the pantry (repositories) manages ingredient storage and retrieval, and the management office (configuration) sets policies and procedures. Each area has a specific purpose and clear boundaries, but they all work together seamlessly. The waitstaff doesn't cook food, the chefs don't handle payments, and the storage manager doesn't serve customers - but information and resources flow efficiently between areas. When a customer orders a meal, the request flows from dining room to kitchen to pantry and back, with each area doing what it does best. A Spring Boot application works the same way, with each package and layer having clear responsibilities while collaborating to fulfill user requests efficiently and maintainably.

Examples

Standard project structure:

src/main/java/com/example/app/
├── Application.java              # Main class
├── controller/                   # Web layer
│   ├── UserController.java
│   └── ProductController.java
├── service/                      # Business layer
│   ├── UserService.java
│   └── ProductService.java
├── repository/                   # Data layer
│   ├── UserRepository.java
│   └── ProductRepository.java
├── model/                        # Domain entities
│   ├── User.java
│   └── Product.java
├── dto/                          # Data transfer objects
│   ├── UserDto.java
│   └── ProductDto.java
└── config/                       # Configuration
    ├── SecurityConfig.java
    └── DatabaseConfig.java

Main application class:

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Package organization by feature:

src/main/java/com/example/app/
├── user/
│   ├── UserController.java
│   ├── UserService.java
│   ├── UserRepository.java
│   └── User.java
└── product/
    ├── ProductController.java
    ├── ProductService.java
    ├── ProductRepository.java
    └── Product.java

Configuration properties structure:

# application.yml organized by component
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/app
    username: ${DB_USER:admin}
  jpa:
    hibernate:
      ddl-auto: validate
  security:
    jwt:
      secret: ${JWT_SECRET}

Layered Architecture

Definition

  • Layered architecture organizes application components into horizontal layers where each layer has specific responsibilities and can only communicate with adjacent layers.
  • The typical layers are presentation (controllers), business (services), persistence (repositories), and domain (entities).
  • This architecture promotes separation of concerns, makes testing easier, and allows changes in one layer without affecting others.
  • Dependencies flow downward, meaning higher layers depend on lower layers but not vice versa.

Analogy

Layered architecture is like a multi-story corporate building where each floor has a specific function and clear communication protocols. The executive floor (presentation layer) makes high-level decisions and communicates with clients, the management floor (business layer) implements company policies and coordinates operations, the operations floor (service layer) handles day-to-day work processes, and the basement (data layer) stores and retrieves company records. Information flows up and down through proper channels - executives don't directly access file cabinets in the basement, and data clerks don't make executive decisions. Each floor focuses on its expertise while relying on the floors below for support. When someone needs information, the request flows down through proper channels until it reaches the right floor, then the response flows back up. This structured approach ensures everyone knows their role, prevents chaos, and makes the organization scalable and maintainable.

Examples

Layer dependency diagram:

Controller Layer (Web)
    ↓ depends on
Service Layer (Business)
    ↓ depends on
Repository Layer (Data)
    ↓ depends on
Domain Layer (Entities)

Controller layer implementation:

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

    @GetMapping("/{id}")
    public UserDto getUser(@PathVariable Long id) {
        return userService.findById(id);  // Delegates to service
    }
}

Service layer with business logic:

@Service
public class UserService {
    private final UserRepository userRepository;

    public UserDto findById(Long id) {
        User user = userRepository.findById(id)
            .orElseThrow(() -> new UserNotFoundException(id));
        return convertToDto(user);  // Business logic
    }
}

Repository layer for data access:

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email);
    List<User> findByActiveTrue();
}

Data Layer

Definition

  • The data layer handles all database interactions and data persistence concerns in your Spring Boot application.
  • It consists of entities (domain models), repositories (data access), and configuration for database connections.
  • This layer abstracts database-specific details from business logic, provides a clean API for data operations, and ensures data consistency and integrity.
  • Spring Data JPA simplifies repository implementation with automatic query generation and custom query support.

Analogy

The data layer is like a professional library system with librarians, catalogs, and storage areas. The entities (books) contain the actual information, the repositories (librarians) know exactly where everything is stored and how to retrieve it efficiently, and the database configuration (library policies) determines how the collection is organized and accessed. When someone needs information, they don't wander through the stacks randomly - they ask the librarian (repository) who uses the catalog system (database indexes) to quickly locate and retrieve the exact resources needed. The librarians handle all the complex filing, cross-referencing, and maintenance tasks, so researchers (business logic) can focus on analyzing information rather than worrying about storage details. This organized approach ensures information is reliable, accessible, and properly maintained.

Examples

Entity with relationships:

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true)
    private String email;

    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
    private List<Order> orders = new ArrayList<>();
}

Repository with custom queries:

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    @Query("SELECT u FROM User u WHERE u.active = true")
    List<User> findActiveUsers();

    @Modifying
    @Query("UPDATE User u SET u.lastLogin = :date WHERE u.id = :id")
    void updateLastLogin(@Param("id") Long id, @Param("date") LocalDateTime date);
}

Database configuration:

@Configuration
@EnableJpaRepositories
public class DatabaseConfig {

    @Bean
    @Primary
    public DataSource dataSource() {
        HikariConfig config = new HikariConfig();
        config.setMaximumPoolSize(20);
        return new HikariDataSource(config);
    }
}

Transaction management:

@Service
@Transactional
public class UserService {

    @Transactional(readOnly = true)
    public List<User> findAll() {
        return userRepository.findAll();
    }

    public User createUser(User user) {
        // Automatic transaction management
        return userRepository.save(user);
    }
}

Service Layer

Definition

  • The service layer contains your application's business logic and coordinates operations between different components.
  • Services encapsulate complex business rules, orchestrate calls to repositories, handle transactions, and transform data between layers.
  • This layer is where your application's core functionality lives, independent of how data is stored or how users interact with the system.
  • Services should be focused, testable, and reusable across different parts of your application.

Analogy

The service layer is like the management team in a company that makes strategic decisions and coordinates between departments. When a customer wants to process a complex order, the sales manager (service) doesn't just pass the request directly to the warehouse (repository). Instead, they check inventory levels, verify customer credit, calculate discounts, coordinate with shipping, and ensure everything happens in the right sequence according to business rules. The manager knows how to handle exceptions - what to do when items are out of stock, how to process rush orders, and when to escalate issues to higher management. They transform raw data (like product codes) into meaningful information (like customer invoices) and ensure that all departments work together smoothly to achieve business objectives. This coordination and decision-making expertise is what makes the business run effectively.

Examples

Service with business logic:

@Service
public class OrderService {
    private final OrderRepository orderRepository;
    private final UserService userService;
    private final PaymentService paymentService;

    public OrderDto createOrder(CreateOrderRequest request) {
        User user = userService.validateUser(request.getUserId());
        Order order = new Order(user, request.getItems());

        if (order.getTotal().compareTo(BigDecimal.valueOf(1000)) > 0) {
            order.setStatus(OrderStatus.PENDING_APPROVAL);
        }

        return convertToDto(orderRepository.save(order));
    }
}

Service coordination:

@Service
public class UserRegistrationService {

    public UserDto registerUser(RegistrationRequest request) {
        validateRequest(request);

        User user = createUser(request);
        sendWelcomeEmail(user);
        logRegistrationEvent(user);

        return convertToDto(user);
    }
}

Service with validation:

@Service
public class ProductService {

    public ProductDto updateProduct(Long id, UpdateProductRequest request) {
        Product product = findProductById(id);

        if (request.getPrice().compareTo(BigDecimal.ZERO) <= 0) {
            throw new InvalidPriceException("Price must be positive");
        }

        product.updateFromRequest(request);
        return convertToDto(productRepository.save(product));
    }
}

Service interfaces for testability:

public interface EmailService {
    void sendWelcomeEmail(User user);
    void sendPasswordResetEmail(User user, String token);
}

@Service
public class EmailServiceImpl implements EmailService {
    // Implementation details
}

Web Layer

Definition

  • The web layer handles HTTP requests and responses, managing the interaction between users and your application.
  • Controllers receive requests, validate input, delegate to services, and return appropriate responses.
  • This layer also includes request/response DTOs, validation annotations, and error handling.
  • The web layer should be thin, focusing on HTTP concerns while delegating business logic to services.
  • It transforms external requests into internal operations and formats results for client consumption.

Analogy

The web layer is like the reception desk and customer service department of a busy office building. When visitors (HTTP requests) arrive, the receptionist (controller) greets them, checks their credentials, determines what they need, and directs them to the right department (service). The receptionist doesn't solve complex problems themselves - they know who to call and how to route requests efficiently. They also handle the communication protocol - translating visitor requests into internal language the organization understands, and then translating responses back into terms visitors can understand. When problems occur, they provide appropriate error messages and guide visitors to alternative solutions. The reception area also maintains visitor logs (request logging) and ensures security protocols are followed before allowing access to internal departments.

Examples

REST controller with validation:

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

    @PostMapping
    public ResponseEntity<UserDto> createUser(@Valid @RequestBody CreateUserRequest request) {
        UserDto user = userService.createUser(request);
        return ResponseEntity.status(HttpStatus.CREATED).body(user);
    }

    @GetMapping("/{id}")
    public UserDto getUser(@PathVariable @Positive Long id) {
        return userService.findById(id);
    }
}

Request/Response DTOs:

public class CreateUserRequest {
    @NotBlank
    @Email
    private String email;

    @NotBlank
    @Size(min = 8)
    private String password;

    @NotBlank
    private String firstName;
}

public class UserDto {
    private Long id;
    private String email;
    private String firstName;
    private LocalDateTime createdAt;
}

Global exception handling:

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleUserNotFound(UserNotFoundException ex) {
        ErrorResponse error = new ErrorResponse("USER_NOT_FOUND", ex.getMessage());
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
        // Handle validation errors
        return ResponseEntity.badRequest().body(createValidationError(ex));
    }
}

Controller integration testing:

@WebMvcTest(UserController.class)
class UserControllerTest {
    @MockBean
    private UserService userService;

    @Test
    void shouldCreateUser() throws Exception {
        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content("""
                    {"email": "test@example.com", "firstName": "John"}
                    """))
                .andExpect(status().isCreated());
    }
}

Configuration Layer

Definition

  • The configuration layer defines how your Spring Boot application components are wired together and how they behave in different environments.
  • Configuration classes use annotations like @Configuration, @Bean, and @ConfigurationProperties to set up beans, define dependencies, and customize framework behavior.
  • This layer separates configuration concerns from business logic, making your application more flexible and easier to deploy across different environments with different settings.

Analogy

The configuration layer is like the setup crew and technical specifications for a theater production. Before the show begins, the crew configures the stage, lighting, sound systems, and backstage equipment according to detailed specifications. They don't perform in the show, but they ensure all the technical elements work together perfectly so the actors can focus on their performances. Different venues might require different configurations - a small theater needs different lighting than a large auditorium - but the same show can adapt to various environments. The configuration crew also sets up communication systems so the director can coordinate with lighting technicians, sound engineers, and stage managers. In Spring Boot applications, configuration classes work similarly, setting up the technical infrastructure so your business components can focus on their core responsibilities while being flexible enough to work in development, testing, and production environments.

Examples

Database configuration:

@Configuration
public class DatabaseConfig {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource dataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    public JpaTransactionManager transactionManager(EntityManagerFactory emf) {
        return new JpaTransactionManager(emf);
    }
}

Security configuration:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                .anyRequest().authenticated())
            .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
            .build();
    }
}

Application properties configuration:

@ConfigurationProperties(prefix = "app")
@Component
public class AppProperties {
    private String name;
    private String version;
    private Security security = new Security();

    public static class Security {
        private String jwtSecret;
        private int jwtExpirationHours = 24;
    }
}

Profile-specific configuration:

# application-dev.yml
spring:
  datasource:
    url: jdbc:h2:mem:devdb
  jpa:
    show-sql: true

---
# application-prod.yml
spring:
  datasource:
    url: jdbc:mysql://prod-server:3306/app
  jpa:
    show-sql: false

Caching Integration

Definition

  • The caching layer improves application performance by temporarily storing frequently accessed data in memory, reducing the need to repeatedly fetch data from the database or recompute results.
  • Spring Boot provides easy integration with caching providers like Ehcache, Hazelcast, and Redis.
  • Caching can be applied at various levels - method results, data queries, or even whole objects - and can significantly speed up response times for read-heavy applications.

Analogy

Caching is like having a well-organized set of frequently used tools and materials within easy reach of a craftsman, instead of having to fetch them from storage every time they are needed. For a chef, caching might mean having commonly used ingredients pre-prepared and stored within arm's reach, so they don't have to chop vegetables or measure spices from scratch for every single dish. In a software application, caching stores copies of data or computed results in a location that can be accessed much more quickly than the original source, whether that's a database, an external service, or a complex computation. Just as a chef can prepare a meal faster with pre-chopped ingredients, an application can respond to requests faster with cached data.

Examples

Method-level caching:

@Service
public class ProductService {

    @Cacheable("products")
    public ProductDto getProduct(Long id) {
        // Expensive operation, e.g. database call
    }
}

Cache configuration:

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        EhCacheManagerFactoryBean factory = new EhCacheManagerFactoryBean();
        factory.setConfigLocation(new ClassPathResource("ehcache.xml"));
        factory.setShared(true);
        return factory.getObject();
    }
}

Cache eviction:

@Service
public class ProductService {

    @CacheEvict(value = "products", allEntries = true)
    public void clearCache() {
        // Cache eviction logic
    }
}

Validation Integration

Definition

  • The validation layer ensures that only valid data enters your system and that business rules are enforced consistently.
  • Spring Boot provides powerful validation features, including JSR-303/JSR-380 Bean Validation, custom validators, and integration with Spring MVC for automatic request validation.
  • Proper validation prevents invalid data from causing errors or inconsistent states within your application.

Analogy

Validation is like having a strict quality control process in a manufacturing line that checks each product against predefined standards before it is allowed to proceed to the next stage or be shipped to customers. Just as a manufacturer wouldn't want defective products to reach customers, leading to dissatisfaction or damage to the brand, an application must ensure that only data that meets all correctness and business rules enters the system. Validation acts as a protective barrier, catching errors and inconsistencies early, whether they are due to user input mistakes, system integration issues, or data corruption.

Examples

Field-level validation:

public class CreateUserRequest {
    @NotBlank
    @Email
    private String email;

    @NotBlank
    @Size(min = 8)
    private String password;
}

Custom validation annotation:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PasswordsMatchValidator.class)
public @interface PasswordsMatch {
    String message() default "Passwords do not match";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

public class PasswordsMatchValidator implements ConstraintValidator<PasswordsMatch, Object> {
    public boolean isValid(Object obj, ConstraintValidatorContext context) {
        // Validation logic
    }
}

Global validation error handling:

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ValidationErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
        ValidationErrorResponse response = new ValidationErrorResponse();
        ex.getBindingResult().getFieldErrors().forEach(error ->
            response.addError(error.getField(), error.getDefaultMessage()));
        return ResponseEntity.badRequest().body(response);
    }
}

Security Integration

Definition

  • Security integration weaves authentication and authorization throughout your Spring Boot application layers.
  • Security filters intercept requests before they reach controllers, security annotations protect service methods, and security context provides user information across the application.
  • Integration includes configuring authentication providers, defining access rules, handling security exceptions, and ensuring security policies are consistently applied across all application components.

Analogy

Security integration is like implementing a comprehensive security system throughout a modern office building. There's not just one security guard at the front door - instead, there are multiple security layers working together seamlessly. Key cards control building access, elevator permissions determine which floors you can reach, door locks protect sensitive areas, and security cameras monitor activity throughout. The security system knows who you are from the moment you enter, and that identity follows you everywhere, automatically granting or denying access based on your role and clearance level. Security personnel don't interrupt normal business operations - they work invisibly in the background, only becoming noticeable when someone tries to access something they shouldn't. Emergency protocols ensure that if a security breach occurs anywhere, the entire system responds appropriately to protect the organization's assets and people.

Examples

Method-level security:

@Service
@PreAuthorize("hasRole('ADMIN')")
public class AdminService {

    @PreAuthorize("hasPermission(#userId, 'User', 'READ')")
    public UserDto getUser(Long userId) {
        return userService.findById(userId);
    }

    @PostAuthorize("returnObject.ownerId == authentication.name")
    public DocumentDto getDocument(Long docId) {
        return documentService.findById(docId);
    }
}

Security context usage:

@Service
public class AuditService {

    public void logUserAction(String action) {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        String username = auth.getName();

        AuditLog log = new AuditLog(username, action, LocalDateTime.now());
        auditRepository.save(log);
    }
}

Custom security annotations:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("@securityService.canAccessResource(#resourceId, authentication)")
public @interface CanAccessResource {
}

@RestController
public class ResourceController {

    @GetMapping("/resources/{id}")
    @CanAccessResource
    public ResourceDto getResource(@PathVariable Long resourceId) {
        return resourceService.findById(resourceId);
    }
}

Security exception handling:

@RestControllerAdvice
public class SecurityExceptionHandler {

    @ExceptionHandler(AccessDeniedException.class)
    public ResponseEntity<ErrorResponse> handleAccessDenied(AccessDeniedException ex) {
        return ResponseEntity.status(HttpStatus.FORBIDDEN)
            .body(new ErrorResponse("ACCESS_DENIED", "Insufficient privileges"));
    }
}

Monitoring Layer

Definition

  • The monitoring layer provides insights into your application's performance, behavior, and overall health in real-time.
  • Spring Boot offers integration with monitoring systems like Prometheus, Grafana, and Micrometer, allowing you to collect and visualize metrics, logs, and traces.
  • Effective monitoring helps you detect issues early, understand usage patterns, and ensure your application is running smoothly and efficiently.

Analogy

Monitoring is like having a comprehensive set of sensors and gauges in a car that provide real-time data on speed, engine temperature, fuel level, and more. Just as a driver relies on these indicators to ensure the car is functioning properly and to detect any potential issues early, developers and operators use monitoring tools to observe the application's vital signs. If any metric goes beyond the normal range - like high CPU usage, low memory, or slow response times - it's a sign that something needs attention. Monitoring helps maintain the application's health, performance, and reliability, just as dashboard indicators help maintain the vehicle's operability and safety.

Examples

Application metrics with Micrometer:

@RestController
public class MetricsController {

    private final MeterRegistry meterRegistry;

    @GetMapping("/api/metrics")
    public List<Meter> listMetrics() {
        return meterRegistry.getMeters();
    }
}

Prometheus configuration:

@Configuration
public class PrometheusConfig {

    @Bean
    public ServletRegistrationBean<PrometheusServlet> prometheusServlet() {
        return new ServletRegistrationBean<>(new PrometheusServlet(), "/actuator/prometheus");
    }
}

Grafana dashboard configuration:

{
  "annotations": {
    "list": [
      {
        "builtIn": 1,
        "datasource": "__expr__",
        "enable": true,
        "hide": true,
        "iconColor": "rgba(255, 255, 255, 0.5)",
        "name": "Annotations & Alerts",
        "type": "annotations"
      }
    ]
  },
  "editable": true,
  "gnetId": null,
  "id": 2,
  "links": [],
  "panels": [
    {
      "datasource": "Prometheus",
      "id": 2,
      "interval": "10s",
      "legendFormat": "{{instance}}",
      "metrics": [
        {
          "expr": "rate(http_requests_total[1m])",
          "interval": "",
          "legendFormat": "requests/sec",
          "refId": "A"
        }
      ],
      "title": "HTTP Requests",
      "type": "graph"
    }
  ],
  "schemaVersion": 26,
  "tags": [],
  "templating": {
    "list": []
  },
  "title": "Application Monitoring",
  "version": 1
}

Exception Handling

Definition

  • Exception handling in a complete Spring Boot application provides consistent error responses across all layers and components.
  • Global exception handlers catch and transform exceptions into appropriate HTTP responses, while custom exceptions carry domain-specific error information.
  • Proper exception handling includes logging, error codes, user-friendly messages, and different handling strategies for different types of errors (validation, business logic, system errors).

Analogy

Exception handling is like having a well-trained crisis management team that handles problems consistently throughout an organization. When something goes wrong anywhere in the company - whether it's a customer complaint, equipment failure, or communication breakdown - the crisis team has established procedures for each type of problem. They don't panic or give inconsistent responses; instead, they follow proven protocols to assess the situation, communicate appropriately with affected parties, document what happened, and implement solutions. Customer-facing staff are trained to provide helpful, professional responses even when internal systems fail, while technical teams work behind the scenes to resolve issues. The crisis management system ensures that problems are handled gracefully, customers receive consistent service, and the organization learns from each incident to prevent similar issues in the future.

Examples

Custom business exceptions:

public class UserNotFoundException extends RuntimeException {
    private final Long userId;

    public UserNotFoundException(Long userId) {
        super("User not found with id: " + userId);
        this.userId = userId;
    }
}

public class InsufficientBalanceException extends RuntimeException {
    private final BigDecimal balance;
    private final BigDecimal required;
}

Comprehensive exception handler:

@RestControllerAdvice
public class GlobalExceptionHandler {
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleUserNotFound(UserNotFoundException ex) {
        logger.warn("User not found: {}", ex.getMessage());
        return ResponseEntity.notFound().build();
    }

    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity<ValidationErrorResponse> handleValidation(ConstraintViolationException ex) {
        ValidationErrorResponse response = new ValidationErrorResponse();
        ex.getConstraintViolations().forEach(violation ->
            response.addError(violation.getPropertyPath().toString(), violation.getMessage()));
        return ResponseEntity.badRequest().body(response);
    }
}

Error response DTOs:

public class ErrorResponse {
    private String code;
    private String message;
    private LocalDateTime timestamp;
    private String path;
}

public class ValidationErrorResponse {
    private Map<String, String> fieldErrors = new HashMap<>();
    private LocalDateTime timestamp = LocalDateTime.now();
}

Layer-specific error handling:

@Service
public class PaymentService {

    public PaymentResult processPayment(PaymentRequest request) {
        try {
            return paymentGateway.charge(request);
        } catch (PaymentGatewayException ex) {
            logger.error("Payment gateway error: {}", ex.getMessage());
            throw new PaymentProcessingException("Payment could not be processed", ex);
        } catch (Exception ex) {
            logger.error("Unexpected error during payment processing", ex);
            throw new SystemException("An unexpected error occurred", ex);
        }
    }
}

Testing Strategy

Definition

  • A comprehensive testing strategy for Spring Boot applications includes unit tests for individual components, integration tests for component interactions, and end-to-end tests for complete workflows.
  • Different testing approaches are used for different layers - @WebMvcTest for controllers, @DataJpaTest for repositories, and @SpringBootTest for full application context.
  • Testing strategy also includes test data management, mocking external dependencies, and automated test execution in CI/CD pipelines.

Analogy

Testing strategy is like quality assurance in aircraft manufacturing, where safety demands multiple levels of rigorous testing. Individual components like engines are tested in isolation (unit tests) to ensure they meet specifications. Then components are tested together (integration tests) to verify they work properly when connected - does the engine communicate correctly with the flight control system? Finally, the complete aircraft undergoes flight testing (end-to-end tests) under real conditions to ensure everything works together safely. Each testing level serves a specific purpose: component testing catches basic defects quickly and cheaply, integration testing reveals interface problems, and flight testing validates that the entire system performs as expected. Test pilots use simulators and controlled environments to test dangerous scenarios safely, just like how we use test databases and mock services to test error conditions without risking production systems.

Examples

Unit testing services:

@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    @Test
    void shouldFindUserById() {
        User user = new User("test@example.com");
        when(userRepository.findById(1L)).thenReturn(Optional.of(user));

        UserDto result = userService.findById(1L);

        assertThat(result.getEmail()).isEqualTo("test@example.com");
    }
}

Integration testing controllers:

@SpringBootTest
@AutoConfigureTestDatabase(replace = Replace.NONE)
@Testcontainers
class UserControllerIntegrationTest {
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:13");

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void shouldCreateAndRetrieveUser() {
        CreateUserRequest request = new CreateUserRequest("test@example.com", "John");

        ResponseEntity<UserDto> response = restTemplate.postForEntity(
            "/api/users", request, UserDto.class);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        assertThat(response.getBody().getEmail()).isEqualTo("test@example.com");
    }
}

Repository testing:

@DataJpaTest
class UserRepositoryTest {
    @Autowired
    private TestEntityManager entityManager;

    @Autowired
    private UserRepository userRepository;

    @Test
    void shouldFindActiveUsers() {
        User activeUser = new User("active@example.com", true);
        User inactiveUser = new User("inactive@example.com", false);
        entityManager.persistAndFlush(activeUser);
        entityManager.persistAndFlush(inactiveUser);

        List<User> result = userRepository.findByActiveTrue();

        assertThat(result).hasSize(1);
        assertThat(result.get(0).getEmail()).isEqualTo("active@example.com");
    }
}

Test configuration:

@TestConfiguration
public class TestConfig {

    @Bean
    @Primary
    public EmailService mockEmailService() {
        return Mockito.mock(EmailService.class);
    }

    @Bean
    @Primary
    public Clock testClock() {
        return Clock.fixed(Instant.parse("2023-01-01T00:00:00Z"), ZoneOffset.UTC);
    }
}

Step-by-Step Build Process

Definition

  • Building a Spring Boot application step-by-step involves creating components in a logical order that allows you to test and validate each piece before adding complexity.
  • The typical progression starts with domain models and repositories, then adds services, controllers, configuration, and finally integration features like security and error handling.
  • This incremental approach helps identify issues early and ensures each layer is working correctly before building upon it.

Analogy

Building a Spring Boot application step-by-step is like constructing a house where you must complete each phase before moving to the next. You start with the foundation (domain models and database), ensuring it's solid and properly designed to support everything that comes after. Then you build the structural framework (repositories and services), which provides the skeleton that holds everything together. Next comes the electrical and plumbing (configuration and integration), followed by the walls and interior (controllers and web layer). Finally, you add finishing touches like security systems and error handling. At each stage, you test and inspect the work - you don't wait until the house is complete to discover the foundation is cracked or the wiring is faulty. Each phase builds upon the previous one, so mistakes early in the process become increasingly expensive to fix as you progress. This methodical approach ensures a solid, well-integrated final product.

Examples

Step 1: Define domain models:

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Email
    @Column(unique = true, nullable = false)
    private String email;

    @NotBlank
    private String firstName;

    // Test with repository before adding complexity
}

Step 2: Create repositories:

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

// Test data access before adding business logic
@DataJpaTest
class UserRepositoryTest {
    // Verify CRUD operations work correctly
}

Step 3: Implement services:

@Service
public class UserService {
    private final UserRepository userRepository;

    public UserDto createUser(CreateUserRequest request) {
        // Business logic implementation
        // Test business rules before adding web layer
    }
}

Step 4: Add web controllers:

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

    @PostMapping
    public ResponseEntity<UserDto> createUser(@Valid @RequestBody CreateUserRequest request) {
        // Web layer implementation
        // Test HTTP endpoints before adding security
    }
}

Step 5: Configure security and integrate:

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    // Add security last, after core functionality works
    // Test security integration thoroughly
}

Component Integration

Definition

  • Component integration ensures all parts of your Spring Boot application work together seamlessly through dependency injection, event handling, and shared configuration.
  • Integration involves managing bean lifecycles, handling cross-cutting concerns like logging and metrics, coordinating transactions across multiple components, and ensuring data consistency throughout the application.
  • Proper integration makes your application behave as a cohesive system rather than a collection of independent parts.

Analogy

Component integration is like coordinating a symphony orchestra where dozens of musicians with different instruments must perform together in perfect harmony. The conductor (Spring container) ensures everyone starts and stops at the right time, follows the same tempo, and responds to dynamic changes during the performance. The sheet music (configuration) provides the common framework that tells each section when to play, but the real magic happens when the violins, brass, woodwinds, and percussion blend together to create something beautiful that none could achieve alone. Each musician is skilled individually, but they must listen to each other, follow cues, and adjust their performance based on what's happening around them. When integration works well, the audience hears one magnificent piece of music, not a cacophony of separate instruments fighting for attention.

Examples

Event-driven integration:

@Component
public class UserEventHandler {

    @EventListener
    public void handleUserRegistration(UserRegisteredEvent event) {
        emailService.sendWelcomeEmail(event.getUser());
        auditService.logUserRegistration(event.getUser());
        notificationService.notifyAdmins(event.getUser());
    }
}

@Service
public class UserService {
    private final ApplicationEventPublisher eventPublisher;

    public UserDto registerUser(RegistrationRequest request) {
        User user = createUser(request);
        eventPublisher.publishEvent(new UserRegisteredEvent(user));
        return convertToDto(user);
    }
}

Cross-cutting concern integration:

@Aspect
@Component
public class LoggingAspect {

    @Around("@annotation(Loggable)")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        try {
            Object result = joinPoint.proceed();
            long duration = System.currentTimeMillis() - start;
            logger.info("Method {} executed in {}ms", joinPoint.getSignature().getName(), duration);
            return result;
        } catch (Exception e) {
            logger.error("Method {} failed after {}ms", joinPoint.getSignature().getName(),
                        System.currentTimeMillis() - start, e);
            throw e;
        }
    }
}

Configuration integration:

@Configuration
public class IntegrationConfig {

    @Bean
    @ConditionalOnProperty(name = "app.features.email", havingValue = "true")
    public EmailService emailService() {
        return new SmtpEmailService();
    }

    @Bean
    @ConditionalOnMissingBean(EmailService.class)
    public EmailService noOpEmailService() {
        return new NoOpEmailService();
    }

    @Bean
    public TaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(4);
        executor.setMaxPoolSize(8);
        return executor;
    }
}

Health check integration:

@Component
public class ApplicationHealthIndicator implements HealthIndicator {
    private final DatabaseHealthService databaseHealth;
    private final ExternalApiHealthService apiHealth;

    @Override
    public Health health() {
        Health.Builder builder = Health.up();

        if (!databaseHealth.isHealthy()) {
            builder.down().withDetail("database", "Connection failed");
        }

        if (!apiHealth.isHealthy()) {
            builder.outOfService().withDetail("external-api", "Service unavailable");
        }

        return builder.build();
    }
}

Summary

Building a complete Spring Boot application requires understanding how all components work together in harmony. You've learned how to structure projects with clear separation of concerns, implement layered architecture with controllers, services, and repositories, and integrate security, configuration, and error handling throughout your application. The step-by-step approach ensures each layer is solid before building upon it, while proper component integration creates a cohesive system that's maintainable, testable, and scalable. This comprehensive understanding prepares you to build professional applications that follow best practices and can grow with your business needs.


Programming Challenge

Build a Complete Task Management Application

Create a Spring Boot application that demonstrates all the concepts covered in this lesson. Your application should include:

  • Domain Models: Task, User, and Project entities with proper relationships
  • Data Layer: Repositories with custom queries for finding tasks by status, user, and project
  • Service Layer: Business logic for task assignment, status transitions, and project management
  • Web Layer: REST controllers with proper validation and error handling
  • Security: JWT authentication with role-based access (USER, PROJECT_MANAGER, ADMIN)
  • Configuration: Profile-specific settings for development and production
  • Testing: Unit tests for services, integration tests for controllers
  • Integration: Event publishing when tasks are completed, email notifications

Focus on proper project structure, clear separation of concerns, and demonstrating how all components work together to create a functional application. Include proper error handling, validation, and security throughout all layers.