Building microservices with Spring Boot can be both powerful and challenging. In this post, I’ll share the essential best practices I’ve learned from building production microservices at scale.

1. Service Design Principles

Single Responsibility

Each microservice should have a single, well-defined responsibility. This makes it easier to:

  • Understand and maintain
  • Scale independently
  • Deploy without affecting other services

Domain-Driven Design

Organize your services around business domains rather than technical layers. This ensures your microservices align with your business capabilities.

2. Configuration Management

External Configuration

Always externalize your configuration:

# application.yml
spring:
  application:
    name: user-service
  profiles:
    active: ${SPRING_PROFILES_ACTIVE:dev}
  datasource:
    url: ${DATABASE_URL:jdbc:postgresql://localhost:5432/users}
    username: ${DATABASE_USERNAME:user}
    password: ${DATABASE_PASSWORD:password}

Environment-Specific Configs

Use Spring profiles to manage different environments:

# Development
java -jar app.jar --spring.profiles.active=dev

# Production
java -jar app.jar --spring.profiles.active=prod

3. Service Communication

REST APIs

For synchronous communication, use REST with proper HTTP status codes:

@RestController
@RequestMapping("/api/v1/users")
public class UserController {
    
    @GetMapping("/{id}")
    public ResponseEntity<User> getUser(@PathVariable Long id) {
        return userService.findById(id)
            .map(user -> ResponseEntity.ok(user))
            .orElse(ResponseEntity.notFound().build());
    }
}

Event-Driven Architecture

For asynchronous communication, use events:

@Component
public class UserEventHandler {
    
    @EventListener
    public void handleUserCreated(UserCreatedEvent event) {
        // Process the event
        notificationService.sendWelcomeEmail(event.getUser());
    }
}

4. Data Management

Database per Service

Each microservice should have its own database. This ensures:

  • Data independence
  • Technology flexibility
  • Reduced coupling

Event Sourcing

Consider event sourcing for audit trails and data consistency:

@Entity
@Table(name = "user_events")
public class UserEvent {
    @Id
    private String eventId;
    private String aggregateId;
    private String eventType;
    private String eventData;
    private LocalDateTime timestamp;
}

5. Monitoring and Observability

Health Checks

Implement comprehensive health checks:

@Component
public class CustomHealthIndicator implements HealthIndicator {
    
    @Override
    public Health health() {
        // Check external dependencies
        if (isDatabaseHealthy() && isExternalServiceHealthy()) {
            return Health.up()
                .withDetail("database", "UP")
                .withDetail("external-service", "UP")
                .build();
        }
        return Health.down()
            .withDetail("database", "DOWN")
            .build();
    }
}

Distributed Tracing

Use Spring Cloud Sleuth for distributed tracing:

spring:
  sleuth:
    zipkin:
      base-url: http://zipkin-server:9411
    sampler:
      probability: 1.0

6. Security

Authentication and Authorization

Implement proper security:

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/actuator/health").permitAll()
                .anyRequest().authenticated()
            )
            .build();
    }
}

7. Testing Strategies

Unit Tests

Write comprehensive unit tests:

@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    
    @Mock
    private UserRepository userRepository;
    
    @InjectMocks
    private UserService userService;
    
    @Test
    void shouldCreateUser() {
        // Given
        User user = new User("John", "[email protected]");
        when(userRepository.save(any(User.class))).thenReturn(user);
        
        // When
        User result = userService.createUser(user);
        
        // Then
        assertThat(result).isNotNull();
        assertThat(result.getName()).isEqualTo("John");
    }
}

Integration Tests

Use TestContainers for integration testing:

@SpringBootTest
@Testcontainers
class UserServiceIntegrationTest {
    
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:13")
            .withDatabaseName("testdb")
            .withUsername("test")
            .withPassword("test");
    
    @Test
    void shouldSaveAndRetrieveUser() {
        // Test implementation
    }
}

Conclusion

Building microservices with Spring Boot requires careful consideration of many factors. By following these best practices, you can create maintainable, scalable, and reliable microservices that serve your business needs effectively.

Remember that microservices are not a silver bullet. Start simple, and only introduce complexity when you have a clear need for it.