Clean Architecture in Java
Introduction
Clean Architecture, coined by Robert C. Martin (Uncle Bob), is a software design philosophy that separates the elements of a design into ring levels. The primary goal is to create systems that are independent of frameworks, UI, databases, and any external agency.
In the context of Java (and specifically enterprise frameworks like Spring Boot), Clean Architecture provides a robust foundation for building highly testable, maintainable, and adaptable applications.
The Core Principles
Clean Architecture revolves around a few fundamental rules:
- Dependency Rule: Source code dependencies must point only inward, toward higher-level policies. Inner circles know nothing about outer circles.
- Separation of Concerns: Dividing the software into layers (e.g., Presentation, Use Cases, Entities).
- Testability: Business rules can be tested without the UI, Database, Web Server, or any other external element.
The Layers of Clean Architecture
Imagine a series of concentric circles. From the center outward, they are:
1. Entities (Enterprise Business Rules)
Entities encapsulate the most general and high-level business rules. They can be objects with methods or a set of data structures and functions.
- Java Context: Plain Old Java Objects (POJOs). No annotations (e.g., no
@Entityor@Tablefrom JPA). - Dependencies: None.
2. Use Cases (Application Business Rules)
The software in this layer contains application-specific business rules. It orchestrates the flow of data to and from the entities.
- Java Context: Interfaces and their implementations (often called Services or Interactors).
- Dependencies: Only depends on Entities.
3. Interface Adapters
This layer converts data from the format most convenient for the use cases and entities into the format most convenient for external agencies like the Web, UI, or Database.
- Java Context: REST Controllers, ViewModels, DTOs, and Database Repository implementations.
- Dependencies: Depends on Use Cases.
4. Frameworks and Drivers
The outermost layer is generally composed of frameworks and tools such as the Database, the Web Framework, etc.
- Java Context: Spring Boot, Hibernate/JPA, Kafka, MySQL.
- Dependencies: Depends on Interface Adapters.
Implementing Clean Architecture in Java
Let's look at a practical example of a user registration flow.
1. Domain Layer (Entities)
The core domain. Absolute zero external dependencies.
// domain/entity/User.java
public class User {
private String id;
private String email;
private String passwordHash;
public User(String id, String email, String passwordHash) {
// Validation logic belongs here (e.g., valid email format)
this.id = id;
this.email = email;
this.passwordHash = passwordHash;
}
// Getters and business methods...
}
2. Use Case Layer (Application)
Defines the interface for the repository (Dependency Inversion) and the business logic.
// usecase/port/UserRepository.java
public interface UserRepository {
void save(User user);
boolean existsByEmail(String email);
}
// usecase/RegisterUserUseCase.java
public class RegisterUserUseCase {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder; // Another interface port
public RegisterUserUseCase(UserRepository userRepository, PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
public void execute(String email, String plainPassword) {
if (userRepository.existsByEmail(email)) {
throw new IllegalArgumentException("User already exists");
}
String hash = passwordEncoder.encode(plainPassword);
User newUser = new User(UUID.randomUUID().toString(), email, hash);
userRepository.save(newUser);
}
}
3. Interface Adapters (Web and Persistence)
Implementing the ports defined by the Use Case layer.
// adapter/persistence/JpaUserRepository.java
import org.springframework.stereotype.Component;
@Component
public class JpaUserRepository implements UserRepository {
private final SpringDataUserRepository springDataRepo;
public JpaUserRepository(SpringDataUserRepository springDataRepo) {
this.springDataRepo = springDataRepo;
}
@Override
public void save(User user) {
// Map Domain User to JPA Entity UserEntity
UserEntity entity = UserMapper.toEntity(user);
springDataRepo.save(entity);
}
@Override
public boolean existsByEmail(String email) {
return springDataRepo.existsByEmail(email);
}
}
// adapter/web/UserController.java
@RestController
@RequestMapping("/users")
public class UserController {
private final RegisterUserUseCase registerUserUseCase;
public UserController(RegisterUserUseCase registerUserUseCase) {
this.registerUserUseCase = registerUserUseCase;
}
@PostMapping("/register")
public ResponseEntity<Void> register(@RequestBody RegisterRequest request) {
registerUserUseCase.execute(request.getEmail(), request.getPassword());
return ResponseEntity.status(HttpStatus.CREATED).build();
}
}
Mapping Between Layers
A common point of friction in Clean Architecture is mapping data between layers. Because dependencies cannot point outward, Entities cannot be used directly as ORM entities (which require database annotations) or Web DTOs (which require JSON annotations).
You will frequently use patterns like Mappers (e.g., MapStruct) to translate:
User(Domain Entity) ↔UserEntity(JPA Entity)RegisterRequest(Web DTO) -> Input for Use Case
While this introduces boilerplate, it protects the core business logic from database schema changes and API contract modifications.
Advantages and Trade-offs
Advantages
- Framework Independence: You can swap Spring for Micronaut or Quarkus without touching the domain logic.
- Database Independence: Swap MySQL for MongoDB easily.
- Highly Testable: The business logic can be unit-tested rapidly without mocking web servers or databases.
Trade-offs
- Complexity and Boilerplate: Clean Architecture requires more classes, interfaces, and mapping files.
- Overkill for Small Apps: For simple CRUD applications, this architecture might be excessive. A simple layered architecture (Controller -> Service -> Repository) might suffice.
Summary
Clean Architecture in Java is about creating boundaries. By utilizing the Dependency Inversion Principle, we ensure that the business rules dictate the tools (databases, frameworks) rather than the tools dictating the business rules.