What Spring Boot Taught Me About Clean Architecture

Reflections on separation of concerns, SOLID principles, and building maintainable Java apps

When I first started working with Spring Boot, I fell in love with how quickly I could build an API.
A few annotations, a couple of endpoints, and — boom — a running service.

But as the project grew, the “magic” that once made things fast began to make debugging harder.
Business logic crept into controllers.
Repositories began to return DTOs.
Testing became a nightmare.

That’s when I learned the most valuable lesson Spring Boot has to teach:
~ Speed is great, but structure is greater.

Let’s talk about what Spring Boot taught me about Clean Architecture, how it aligns with SOLID principles, and how it completely changed the way I build software.

The Problem: When Everything Talks to Everything

My early code looked like this:

@RestController
@RequestMapping("/users")
public class UserController {

@Autowired
private UserRepository userRepository;

@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
// Validation, business logic, persistence — all here 😬
if (user.getEmail() == null || user.getEmail().isEmpty()) {
throw new IllegalArgumentException("Email is required");
}
user.setCreatedAt(LocalDateTime.now());
return ResponseEntity.ok(userRepository.save(user));
}
}

Everything was in one place:

  • The Controller validated data
  • Applied business logic
  • Talked directly to the database

It worked — until it didn’t.
When requirements changed, every file needed editing. Testing was slow and brittle.

The Shift: Understanding Clean Architecture

Then I discovered Clean Architecture — the idea popularized by Uncle Bob (Robert C. Martin).

It’s built on one powerful principle:

Depend on abstractions, not concretions.

Your application should have layers, each with a clear purpose and direction of dependency.

In Spring Boot, a clean architecture often looks like this:

com.example.app
├── controller // Handles HTTP & input/output
├── service // Business logic lives here
├── repository // Database persistence
├── model // Domain models
└── dto // Data transfer objects

Applying It in Spring Boot

Let’s refactor the messy controller above into a cleaner structure.

Domain Model

@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String email;
private String name;
private LocalDateTime createdAt;

// getters and setters
}

Repository Layer

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

Service Layer (Business Logic)

@Service
public class UserService {

private final UserRepository repository;

public UserService(UserRepository repository) {
this.repository = repository;
}

public User createUser(User user) {
if (user.getEmail() == null || user.getEmail().isEmpty()) {
throw new IllegalArgumentException("Email is required");
}
if (repository.findByEmail(user.getEmail()).isPresent()) {
throw new IllegalStateException("User already exists");
}
user.setCreatedAt(LocalDateTime.now());
return repository.save(user);
}
}

Controller Layer (I/O Only)

@RestController
@RequestMapping("/users")
public class UserController {

private final UserService service;

public UserController(UserService service) {
this.service = service;
}

@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
return ResponseEntity.ok(service.createUser(user));
}
}

Now:

  • The controller doesn’t care how users are stored.
  • The service doesn’t care how HTTP works.
  • The repository only manages persistence.

The SOLID Connection

Clean Architecture isn’t just about folders — it’s about principles.
Here’s how Spring Boot helped me internalize the SOLID design philosophy:

| Principle | Meaning                        | Spring Boot Lesson                             |
|------------|--------------------------------|------------------------------------------------|
| S | Single Responsibility | Controller for HTTP, Service for business logic |
| O | Open/Closed | Use interfaces and `@Service` beans |
| L | Liskov Substitution | Interfaces + dependency injection |
| I | Interface Segregation | Define narrow service interfaces |
| D | Dependency Inversion | Autowire interfaces instead of concretes |

Spring Boot makes these principles natural — but only if you design consciously.

Testing Becomes a Joy (Not a Burden)

With this structure, testing became simple.

Instead of starting the whole Spring context, I could test each layer in isolation:

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

@Mock
private UserRepository repository;

@InjectMocks
private UserService service;

@Test
void shouldCreateUserSuccessfully() {
User user = new User();
user.setEmail("[email protected]");

when(repository.findByEmail("[email protected]")).thenReturn(Optional.empty());
when(repository.save(any(User.class))).thenAnswer(i -> i.getArgument(0));

User saved = service.createUser(user);

assertNotNull(saved.getCreatedAt());
verify(repository).save(user);
}
}

No web server, no database — just the logic.
That’s clean architecture in action.

The Takeaways

After building multiple production systems with Spring Boot, here’s what I’ve learned:

  1. Annotations don’t replace architecture.
    Spring Boot makes things easier, not automatic. Design still matters.

2. Separate logic early.
Don’t wait until the controller has 500 lines of code before refactoring.

3. Test by contract, not by side effects.
Unit tests should assert behaviors, not database writes.

4. Let Spring manage dependencies — but own your design.

5. Clean code scales — messy code burns out teams.

Final Thoughts

Spring Boot gave me speed — but Clean Architecture gave me sanity.
If I had to summarize my journey in one sentence:

“Spring Boot taught me that architecture isn’t a restriction; it’s the freedom to change without fear.”

So the next time you start a new Spring Boot project, ask yourself — 
Are you just building fast, or are you building clean?

Recommended Next Steps

  • Refactor one of your old controllers into proper layers.
  • Add one unit test per service.
  • Re-read your code and ask: “Who should really be doing this job?


What Spring Boot Taught Me About Clean Architecture was originally published in Javarevisited on Medium, where people are continuing the conversation by highlighting and responding to this story.

This post first appeared on Read More