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

AspectDetails
FocusSpring Boot 3.x clean code patterns
ArchitectureLayered (Controller → Service → Repository)
Key PrinciplesConstructor injection, thin controllers, rich domain
Common IssuesGod 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)
Code
Loading syntax highlighter...

Pattern 2: Clean Service Layer

JAVA(72 lines)
Code
Loading syntax highlighter...

Pattern 3: Constructor Injection

JAVA(44 lines)
Code
Loading syntax highlighter...

Pattern 4: Configuration Classes

JAVA(68 lines)
Code
Loading syntax highlighter...

Pattern 5: Repository Pattern

JAVA(78 lines)
Code
Loading syntax highlighter...

Pattern 6: Event-Driven Communication

JAVA(64 lines)
Code
Loading syntax highlighter...

Pattern 7: Clean Exception Handling

JAVA(65 lines)
Code
Loading syntax highlighter...

Pattern 8: Testing Spring Applications

JAVA(80 lines)
Code
Loading syntax highlighter...

Common Anti-Patterns

Anti-PatternProblemSolution
God ServiceOne service does everythingSplit by domain
Anemic DomainEntities are just data holdersRich domain model
Field InjectionHidden dependenciesConstructor injection
Fat ControllerBusiness logic in controllerDelegate to service
Circular DependenciesA→B→AIntroduce 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)
Code
Loading 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)
Code
Loading 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:
  1. Inventory reservation is outside any transaction - if payment or order creation fails, inventory stays reserved
  2. 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)
Code
Loading syntax highlighter...
Solution 2: Separate services (better for SRP)
JAVA(28 lines)
Code
Loading syntax highlighter...
Solution 3: Self-injection (use sparingly)
JAVA(21 lines)
Code
Loading 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)
Code
Loading syntax highlighter...
✅ Solution:
JAVA(66 lines)
Code
Loading 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)
Code
Loading syntax highlighter...
✅ Solution:
JAVA(108 lines)
Code
Loading syntax highlighter...

Exercise 3: Event-Driven Decoupling

⭐⭐ Difficulty: Medium | ⏱️ Time: 20 minutes

Task: Refactor this tightly coupled service to use events.
JAVA(31 lines)
Code
Loading syntax highlighter...
✅ Solution:
JAVA(134 lines)
Code
Loading 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)
Code
Loading syntax highlighter...
✅ Solution:
JAVA(177 lines)
Code
Loading 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)
Code
Loading syntax highlighter...
✅ Solution:
JAVA(216 lines)
Code
Loading syntax highlighter...

📝 Summary

LayerResponsibility
ControllerHTTP handling, request/response mapping
ServiceBusiness logic, transaction boundaries
RepositoryData access, queries
DomainBusiness rules, validation
ConfigurationExternal config, bean definitions

📅 Review Schedule for This Article

DayTaskTime
Day 1Review the Layered Architecture responsibilities5 min
Day 3Redo Exercise 1 (Thin Controller Refactoring)10 min
Day 7Audit one controller in your codebase for business logic15 min
Day 14Redo Debug This (The Transaction That Never Was)10 min
Day 30Implement event-driven decoupling in a real service25 min

Next: [Part 25: Decision Guide & Cheatsheet]