Java
Clean Code in Spring Boot
Spring Boot enables rapid development but can lead to messy code if not used thoughtfully. This article covers clean architecture patterns specifically for Spring Boot applications: layered architecture, dependency injection best practices, and avoiding common Spring anti-patterns.
📋 At a Glance
| Aspect | Details |
|---|---|
| Focus | Spring Boot 3.x clean code patterns |
| Architecture | Layered (Controller → Service → Repository) |
| Key Principles | Constructor injection, thin controllers, rich domain |
| Common Issues | God services, anemic domain, circular dependencies |
🎯 What You'll Learn
- Clean layered architecture in Spring Boot
- Dependency injection best practices
- Configuration patterns
- Common anti-patterns and fixes
- Testing strategies for Spring applications
🔬 Deep Dive
Pattern 1: Clean Controller Layer
JAVA(72 lines)CodeLoading syntax highlighter...
Pattern 2: Clean Service Layer
JAVA(72 lines)CodeLoading syntax highlighter...
Pattern 3: Constructor Injection
JAVA(44 lines)CodeLoading syntax highlighter...
Pattern 4: Configuration Classes
JAVA(68 lines)CodeLoading syntax highlighter...
Pattern 5: Repository Pattern
JAVA(78 lines)CodeLoading syntax highlighter...
Pattern 6: Event-Driven Communication
JAVA(64 lines)CodeLoading syntax highlighter...
Pattern 7: Clean Exception Handling
JAVA(65 lines)CodeLoading syntax highlighter...
Pattern 8: Testing Spring Applications
JAVA(80 lines)CodeLoading syntax highlighter...
Common Anti-Patterns
| Anti-Pattern | Problem | Solution |
|---|---|---|
| God Service | One service does everything | Split by domain |
| Anemic Domain | Entities are just data holders | Rich domain model |
| Field Injection | Hidden dependencies | Constructor injection |
| Fat Controller | Business logic in controller | Delegate to service |
| Circular Dependencies | A→B→A | Introduce interface or event |
🐛 Debug This: The Transaction That Never Was
A developer reports: "My order processing fails intermittently. Sometimes items get reserved in inventory but the order isn't created. I have @Transactional on my method - why isn't everything rolling back?"
JAVA(52 lines)CodeLoading syntax highlighter...
When
createOrderInternal throws an exception, the inventory is still reserved but no order exists. Why doesn't @Transactional roll back?✅ Solution:
Problem: Self-invocation bypasses the proxy
JAVA(4 lines)CodeLoading syntax highlighter...
Spring AOP creates a proxy around your bean. When you call
createOrderInternal from within the same class, you bypass the proxy - the @Transactional annotation is never intercepted!External call: Controller → Proxy → OrderService.processOrder() Internal call: OrderService.processOrder() → OrderService.createOrderInternal() ↑ Direct call, no proxy!
Additional problems:
- Inventory reservation is outside any transaction - if payment or order creation fails, inventory stays reserved
- Payment is not in a transaction - if order creation fails, customer is charged but has no order
Solutions:
Solution 1: Move @Transactional to the public method
JAVA(23 lines)CodeLoading syntax highlighter...
Solution 2: Separate services (better for SRP)
JAVA(28 lines)CodeLoading syntax highlighter...
Solution 3: Self-injection (use sparingly)
JAVA(21 lines)CodeLoading syntax highlighter...
The lesson:
@Transactional only works when called from outside the class. Self-invocation bypasses the proxy. Put @Transactional on public entry points, or use separate services.💻 Exercises
Exercise 1: Thin Controller Refactoring
⭐ Difficulty: Easy | ⏱️ Time: 10 minutes
Task: Refactor this fat controller to follow the thin controller pattern.
JAVA(47 lines)CodeLoading syntax highlighter...
✅ Solution:
JAVA(66 lines)CodeLoading syntax highlighter...
Exercise 2: Configuration Properties
⭐⭐ Difficulty: Medium | ⏱️ Time: 15 minutes
Task: Convert these scattered configurations to a type-safe configuration class.
JAVA(28 lines)CodeLoading syntax highlighter...
✅ Solution:
JAVA(108 lines)CodeLoading syntax highlighter...
Exercise 3: Event-Driven Decoupling
⭐⭐ Difficulty: Medium | ⏱️ Time: 20 minutes
Task: Refactor this tightly coupled service to use events.
JAVA(31 lines)CodeLoading syntax highlighter...
✅ Solution:
JAVA(134 lines)CodeLoading syntax highlighter...
Exercise 4: Repository with Custom Queries
⭐⭐⭐ Difficulty: Medium-Hard | ⏱️ Time: 20 minutes
Task: Create a clean repository with dynamic search capabilities.
JAVA(5 lines)CodeLoading syntax highlighter...
✅ Solution:
JAVA(177 lines)CodeLoading syntax highlighter...
Exercise 5: Full Integration Test Suite
⭐⭐⭐⭐ Difficulty: Hard | ⏱️ Time: 30 minutes
Task: Write a comprehensive integration test for an order creation flow.
JAVA(6 lines)CodeLoading syntax highlighter...
✅ Solution:
JAVA(216 lines)CodeLoading syntax highlighter...
📝 Summary
| Layer | Responsibility |
|---|---|
| Controller | HTTP handling, request/response mapping |
| Service | Business logic, transaction boundaries |
| Repository | Data access, queries |
| Domain | Business rules, validation |
| Configuration | External config, bean definitions |
📅 Review Schedule for This Article
| Day | Task | Time |
|---|---|---|
| Day 1 | Review the Layered Architecture responsibilities | 5 min |
| Day 3 | Redo Exercise 1 (Thin Controller Refactoring) | 10 min |
| Day 7 | Audit one controller in your codebase for business logic | 15 min |
| Day 14 | Redo Debug This (The Transaction That Never Was) | 10 min |
| Day 30 | Implement event-driven decoupling in a real service | 25 min |
Next: [Part 25: Decision Guide & Cheatsheet]