Spring Boot Best Practices: Building Production-Ready Applications

Spring Boot is the most popular Java framework for building enterprise applications. Here are best practices for building production-ready Spring Boot applications. 1. Project Structure Recommended Structure src/ ├── main/ │ ├── java/ │ │ └── com/example/ │ │ ├── Application.java │ │ ├── config/ │ │ ├── controller/ │ │ ├── service/ │ │ ├── repository/ │ │ ├── model/ │ │ └── dto/ │ └── resources/ │ ├── application.yml │ └── application-prod.yml └── test/ 2. Configuration Management Use YAML for Configuration # application.yml spring: datasource: url: jdbc:postgresql://localhost:5432/mydb username: ${DB_USERNAME} password: ${DB_PASSWORD} jpa: hibernate: ddl-auto: validate show-sql: false properties: hibernate: format_sql: true server: port: 8080 error: include-message: always include-stacktrace: on_param Profile-Based Configuration # application-dev.yml spring: datasource: url: jdbc:h2:mem:testdb jpa: show-sql: true # application-prod.yml spring: datasource: url: ${DATABASE_URL} jpa: show-sql: false 3. Dependency Injection Constructor Injection // Good: Constructor injection @Service public class UserService { private final UserRepository userRepository; private final EmailService emailService; public UserService(UserRepository userRepository, EmailService emailService) { this.userRepository = userRepository; this.emailService = emailService; } } // Bad: Field injection @Service public class UserService { @Autowired private UserRepository userRepository; } 4. Exception Handling Global Exception Handler @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(ResourceNotFoundException.class) public ResponseEntity<ErrorResponse> handleNotFound( ResourceNotFoundException ex) { ErrorResponse error = new ErrorResponse( HttpStatus.NOT_FOUND.value(), ex.getMessage() ); return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error); } @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<ErrorResponse> handleValidation( MethodArgumentNotValidException ex) { Map<String, String> errors = new HashMap<>(); ex.getBindingResult().getFieldErrors().forEach(error -> errors.put(error.getField(), error.getDefaultMessage()) ); ErrorResponse error = new ErrorResponse( HttpStatus.BAD_REQUEST.value(), "Validation failed", errors ); return ResponseEntity.badRequest().body(error); } } 5. REST API Design Controller Best Practices @RestController @RequestMapping("/api/v1/users") @Validated public class UserController { private final UserService userService; @GetMapping public ResponseEntity<List<UserDTO>> getAllUsers( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size) { List<UserDTO> users = userService.getAllUsers(page, size); return ResponseEntity.ok(users); } @GetMapping("/{id}") public ResponseEntity<UserDTO> getUser(@PathVariable Long id) { UserDTO user = userService.getUserById(id); return ResponseEntity.ok(user); } @PostMapping public ResponseEntity<UserDTO> createUser( @Valid @RequestBody CreateUserRequest request) { UserDTO user = userService.createUser(request); return ResponseEntity.status(HttpStatus.CREATED).body(user); } @PutMapping("/{id}") public ResponseEntity<UserDTO> updateUser( @PathVariable Long id, @Valid @RequestBody UpdateUserRequest request) { UserDTO user = userService.updateUser(id, request); return ResponseEntity.ok(user); } @DeleteMapping("/{id}") public ResponseEntity<Void> deleteUser(@PathVariable Long id) { userService.deleteUser(id); return ResponseEntity.noContent().build(); } } 6. Service Layer Service Implementation @Service @Transactional public class UserService { private final UserRepository userRepository; private final UserMapper userMapper; public UserDTO getUserById(Long id) { User user = userRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException( "User not found with id: " + id)); return userMapper.toDTO(user); } public UserDTO createUser(CreateUserRequest request) { if (userRepository.existsByEmail(request.getEmail())) { throw new DuplicateResourceException( "Email already exists"); } User user = userMapper.toEntity(request); user = userRepository.save(user); return userMapper.toDTO(user); } } 7. Repository Layer JPA Repository @Repository public interface UserRepository extends JpaRepository<User, Long> { Optional<User> findByEmail(String email); boolean existsByEmail(String email); @Query("SELECT u FROM User u WHERE u.status = :status") List<User> findByStatus(@Param("status") UserStatus status); @Modifying @Query("UPDATE User u SET u.status = :status WHERE u.id = :id") int updateStatus(@Param("id") Long id, @Param("status") UserStatus status); } 8. Entity Design JPA Entity @Entity @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, unique = true) private String email; @Column(nullable = false) private String name; @Enumerated(EnumType.STRING) private UserStatus status; @CreatedDate private LocalDateTime createdAt; @LastModifiedDate private LocalDateTime updatedAt; @Version private Long version; // Getters and setters } 9. Validation DTO Validation public class CreateUserRequest { @NotBlank(message = "Email is required") @Email(message = "Invalid email format") private String email; @NotBlank(message = "Name is required") @Size(min = 2, max = 100, message = "Name must be between 2 and 100 characters") private String name; @Min(value = 18, message = "Age must be at least 18") @Max(value = 120, message = "Age must be at most 120") private Integer age; // Getters and setters } 10. Security Spring Security Configuration @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf().disable() .authorizeHttpRequests(auth -> auth .requestMatchers("/api/public/**").permitAll() .requestMatchers("/api/admin/**").hasRole("ADMIN") .anyRequest().authenticated() ) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) .oauth2ResourceServer(oauth2 -> oauth2 .jwt(jwt -> jwt.decoder(jwtDecoder())) ); return http.build(); } } 11. Testing Unit Tests @ExtendWith(MockitoExtension.class) class UserServiceTest { @Mock private UserRepository userRepository; @InjectMocks private UserService userService; @Test void shouldCreateUser() { // Given CreateUserRequest request = new CreateUserRequest(); request.setEmail("[email protected]"); request.setName("Test User"); User savedUser = new User(); savedUser.setId(1L); savedUser.setEmail(request.getEmail()); when(userRepository.existsByEmail(request.getEmail())).thenReturn(false); when(userRepository.save(any(User.class))).thenReturn(savedUser); // When UserDTO result = userService.createUser(request); // Then assertThat(result.getId()).isEqualTo(1L); assertThat(result.getEmail()).isEqualTo("[email protected]"); } } Integration Tests @SpringBootTest @AutoConfigureMockMvc class UserControllerIntegrationTest { @Autowired private MockMvc mockMvc; @Autowired private UserRepository userRepository; @Test void shouldCreateUser() throws Exception { CreateUserRequest request = new CreateUserRequest(); request.setEmail("[email protected]"); request.setName("Test User"); mockMvc.perform(post("/api/v1/users") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isCreated()) .andExpect(jsonPath("$.email").value("[email protected]")); } } 12. Performance Optimization Connection Pooling spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000 Caching @Configuration @EnableCaching public class CacheConfig { @Bean public CacheManager cacheManager() { return new ConcurrentMapCacheManager("users", "posts"); } } // Usage @Service public class UserService { @Cacheable(value = "users", key = "#id") public UserDTO getUserById(Long id) { return userRepository.findById(id) .map(userMapper::toDTO) .orElseThrow(); } @CacheEvict(value = "users", key = "#id") public void deleteUser(Long id) { userRepository.deleteById(id); } } 13. Monitoring and Logging Actuator Endpoints management: endpoints: web: exposure: include: health,info,metrics,prometheus endpoint: health: show-details: when-authorized Logging Configuration logging: level: root: INFO com.example: DEBUG pattern: console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n" file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" file: name: logs/application.log max-size: 10MB max-history: 30 Best Practices Summary Use constructor injection for dependencies Implement global exception handling Validate all inputs with Bean Validation Use DTOs to separate API from domain models Implement proper logging and monitoring Use profiles for environment-specific config Write comprehensive tests Optimize database queries Implement caching where appropriate Follow RESTful conventions Conclusion Spring Boot best practices help you build: ...

December 10, 2025 · 3327 views

Rate Limiting Java REST APIs

Approaches Token bucket for burst+steady control; sliding window for fairness. Enforce at edge (gateway/ingress) plus app-level for per-tenant safety. Spring implementation Use filters/interceptors with Redis/Lua for atomic buckets. Key by tenant/user/IP; return 429 with Retry-After. Expose metrics per key and rule; alert on near-capacity. Considerations Separate auth failures from rate limits; avoid blocking login endpoints too aggressively. Keep rule configs dynamic; hot-reload from config store. Combine with circuit breakers/timeouts for upstream dependencies. Checklist Edge and app-level limits defined. Redis-based atomic counters/buckets with TTL. Metrics + logs for limit decisions; alerts in place.

February 15, 2025 · 3111 views

Spring Boot Observability: Metrics, Traces, Logs

Metrics Use Micrometer + Prometheus: management.endpoints.web.exposure.include=prometheus,health,info. Add JVM+Tomcat/db pool meters; set percentiles for latencies. Create SLIs: request latency, error rate, saturation (threads/connections), GC pauses. Traces Spring Boot 3 ships with OTel starter: add spring-boot-starter-actuator + micrometer-tracing-bridge-otel + exporter (OTLP/Zipkin/Jaeger). Propagate headers (traceparent); ensure async executors use ContextPropagatingExecutor. Sample smartly: lower rates on noisy paths; raise for errors. Logs Use JSON layout; include traceId/spanId for correlation. Avoid verbose INFO in hot paths; keep payload size bounded. Dashboards & alerts Latency/error SLO dashboards per endpoint. DB pool saturation, thread pool queue depth, GC pause, heap used %, 5xx rate. Alerts on SLO burn rates; include exemplars linking metrics → traces → logs. Checklist Actuator endpoints secured and exposed only where needed. OTLP exporter configured; sampling tuned. Trace/log correlation verified in staging. Dashboards + alerts reviewed with oncall.

January 8, 2025 · 4193 views

Tuning Kafka Consumers (Java)

Core settings max.poll.interval.ms sized to processing time; max.poll.records to batch size. fetch.min.bytes/fetch.max.wait.ms to trade latency vs throughput. enable.auto.commit=false; commit sync/async after processing batch. Concurrency Prefer multiple consumer instances over massive max.poll.records. For CPU-bound steps, hand off to bounded executor; avoid blocking poll thread. Ordering & retries Keep partition affinity when ordering matters; use DLT for poison messages. Backoff with jitter on retries; limit attempts per message. Observability Metrics: lag per partition, commit latency, rebalances, processing time, error rates. Log offsets and partition for errors; trace batch sizes. Checklist Poll loop never blocks; work delegated to bounded pool. Commits after successful processing; DLT in place. Lag and rebalance metrics monitored.

November 22, 2024 · 4864 views

Circuit Breakers with Resilience4j

Core settings Sliding window (count/time), failure rate threshold, slow-call threshold, minimum calls. Wait duration in open state; half-open permitted calls; automatic transition. Patterns Wrap HTTP/DB/queue clients; combine with timeouts/retries/bulkheads. Tune per dependency; differentiate fast-fail vs. tolerant paths. Provide fallback only when safe/idempotent. Observability Export metrics: state changes, calls/success/failure/slow, not permitted count. Log state transitions; add exemplars linking to traces. Alert on frequent open/half-open oscillation. Checklist Per-downstream breaker with tailored thresholds. Timeouts and retries composed correctly (timeout → breaker → retry). Metrics/logs/traces wired; alerts on open rate.

October 30, 2024 · 3804 views

Java GC Tuning: G1 and ZGC in Practice

Choosing G1: balanced latency/throughput for heaps 4–64GB; predictable pauses. ZGC: sub-10ms pauses on large heaps; great for latency-sensitive APIs; slightly higher CPU. Baseline flags G1: -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+ParallelRefProcEnabled -XX:+AlwaysPreTouch ZGC: -XX:+UseZGC -XX:+ZGenerational -XX:+AlwaysPreTouch Set -Xms = -Xmx for stable footprint; size heap from prod RSS data. Metrics to watch Pause p95/p99, GC CPU %, allocation rate, remembered set size (G1), heap occupancy. STW reasons: promotion failure, humongous allocations (G1), metaspace growth. Common fixes Reduce humongous allocations: avoid giant byte[]; use chunked buffers. Lower pause targets only after measuring; avoid over-constraining MaxGCPauseMillis. Cap thread counts: -XX:ParallelGCThreads, -XX:ConcGCThreads if CPU saturated. For ZGC, ensure kernel pages hugepage-friendly; watch NUMA pinning. Checklist Heap sized from live data; -Xms = -Xmx. GC logs on (JDK 17+): -Xlog:gc*:tags,level,time,uptime:file=gc.log:utctime,filesize=20M,files=10 Dashboards for pause/CPU/allocation. Load test changes before prod; compare pause histograms release to release.

October 5, 2024 · 4650 views

Java Class Initialization Order: Understanding the Real Sequence

Understanding Java class initialization order is crucial for avoiding subtle bugs. Here’s the real initialization sequence and common pitfalls. Initialization Order Static Fields and Blocks public class Example { // 1. Static fields initialized first private static int staticField = initializeStatic(); // 2. Static blocks executed in order static { System.out.println("Static block 1"); } static { System.out.println("Static block 2"); } private static int initializeStatic() { System.out.println("Static field initialization"); return 1; } } Instance Fields and Constructors public class Example { // 3. Instance fields initialized private int instanceField = initializeInstance(); // 4. Instance initialization blocks { System.out.println("Instance block"); } // 5. Constructor executed last public Example() { System.out.println("Constructor"); } private int initializeInstance() { System.out.println("Instance field initialization"); return 1; } } Complete Initialization Sequence public class InitializationDemo { // Step 1: Static fields private static String staticField = "Static field"; // Step 2: Static blocks (in order) static { System.out.println("Static block 1"); } static { System.out.println("Static block 2"); } // Step 3: Instance fields private String instanceField = "Instance field"; // Step 4: Instance blocks { System.out.println("Instance block"); } // Step 5: Constructor public InitializationDemo() { System.out.println("Constructor"); } } Common Pitfalls 1. Forward References // Bad: Forward reference public class BadExample { private int value = getValue(); // Error: forward reference private int multiplier = 10; private int getValue() { return multiplier * 2; // multiplier might not be initialized } } // Good: Initialize in correct order public class GoodExample { private int multiplier = 10; private int value = getValue(); private int getValue() { return multiplier * 2; } } 2. Static vs Instance Initialization public class ConfusingExample { private static int staticCounter = 0; private int instanceCounter = 0; static { staticCounter++; // Cannot access instanceCounter here! } { instanceCounter++; staticCounter++; // Can access static } } 3. Inheritance Initialization class Parent { static { System.out.println("Parent static block"); } { System.out.println("Parent instance block"); } public Parent() { System.out.println("Parent constructor"); } } class Child extends Parent { static { System.out.println("Child static block"); } { System.out.println("Child instance block"); } public Child() { System.out.println("Child constructor"); } } // Output order: // Parent static block // Child static block // Parent instance block // Parent constructor // Child instance block // Child constructor Best Practices Initialize fields in logical order Avoid forward references Keep initialization blocks simple Use constructors for complex initialization Document initialization dependencies Conclusion Understanding initialization order helps you: ...

June 15, 2024 · 4708 views

Java Virtual Threads (Loom) for IO-heavy Services

When it shines IO-heavy workloads with many concurrent requests. Simplifies thread-per-request code without callback hell. Great for blocking JDBC (with drivers that release threads), HTTP clients, and file IO. Caveats Avoid blocking operations that pin VTs (synchronized blocks, some native calls). Watch libraries that block on locks; prefer async-friendly drivers when possible. Pinning shows as carrier thread exhaustion; monitor. Usage Executors: Executors.newVirtualThreadPerTaskExecutor(). For servers (e.g., Spring): set spring.threads.virtual.enabled=true (Spring Boot 3.2+). Keep per-request timeouts; use structured concurrency where possible. Observability Metrics: carrier thread pool usage, VT creation rate, blocked/pinned threads. Profiling: use JDK Flight Recorder; check for pinning events. Checklist Dependencies vetted for blocking/pinning. Timeouts on all IO; circuit breakers still apply. Dashboards for carrier thread utilization and pinning. Load test before prod; compare throughput/latency vs platform threads.

April 18, 2024 · 4328 views

Hibernate: Fixing 'Object References an Unsaved Transient Instance' Error

The “object references an unsaved transient instance” error is common in Hibernate. Here’s how to fix it. Understanding the Error This error occurs when you try to save an entity that references another entity that hasn’t been persisted yet. // Error scenario User user = new User(); // Transient (not saved) Post post = new Post(); post.setUser(user); // References unsaved user postRepository.save(post); // Error! Solutions 1. Save Parent First // Save user first User user = new User(); user = userRepository.save(user); // Now managed Post post = new Post(); post.setUser(user); postRepository.save(post); // Works! 2. Use Cascade Types @Entity public class Post { @ManyToOne(cascade = CascadeType.PERSIST) private User user; } // Now saving post will save user too Post post = new Post(); post.setUser(new User()); postRepository.save(post); // Works! 3. Use @OneToMany with Cascade @Entity public class User { @OneToMany(mappedBy = "user", cascade = CascadeType.ALL) private List<Post> posts; } User user = new User(); Post post = new Post(); user.getPosts().add(post); post.setUser(user); userRepository.save(user); // Saves both Common Patterns Bidirectional Relationships @Entity public class User { @OneToMany(mappedBy = "user", cascade = CascadeType.ALL) private List<Post> posts = new ArrayList<>(); } @Entity public class Post { @ManyToOne @JoinColumn(name = "user_id") private User user; // Helper method public void setUser(User user) { this.user = user; if (user != null && !user.getPosts().contains(this)) { user.getPosts().add(this); } } } Best Practices Save parent entities first Use appropriate cascade types Maintain bidirectional relationships Use helper methods for relationships Handle null references Conclusion Fix Hibernate transient instance errors by: ...

April 10, 2024 · 4068 views