In modern software development, complexity grows quickly. Without the right structure, codebases can become fragile, difficult to test, and costly to maintain. Clean Architecture offers a clear design philosophy that addresses these challenges head-on by keeping your core business logic pure, independent from frameworks, databases, and other external tools. As a result, your application remains maintainable, testable, and adaptable, even as technology evolves.
This article examines the principles of Clean Architecture, its structural organization, and a practical example of its implementation.
Defining Clean Architecture
Formal Definition:
Clean Architecture is a software design philosophy that organizes an application into distinct, independent layers. Its main goal is to separate core business rules from implementation details such as frameworks, databases, or user interfaces.
By applying this separation, you can:
- Replace a database without touching your business logic.
- Swap out a web framework without rewriting the entire system.
- Test core rules without relying on slow, brittle external systems.
The Structure of Clean Architecture
Visualize Clean Architecture as four concentric circles, each representing a layer with a specific responsibility:
Entities – Enterprise Business Rules
- Contain core domain models and rules.
- Independent of frameworks, databases, or libraries.
- Designed to be stable and reusable across multiple applications.
Use Cases – Application Business Rules
- Define how the application responds to inputs.
- Coordinate between entities and business rules.
- Remain unaware of the web, UI, or database details.
Interface Adapters
- Contain controllers, presenters, gateways, and mappers.
- Adapt data between external systems and internal layers.
- Translate input/output into forms that the core application understands.
Frameworks & Drivers
- Include databases, web frameworks, file systems, or other external tools.
- Considered “details” rather than the essence of the application.
- Can be swapped without affecting the business logic.
The Importance of Clean Architecture
Separation of Concerns: Each layer focuses on a single purpose, making the system easier to maintain and extend.
Testability: Business rules can be tested without databases or frameworks.
Flexibility: Change frameworks, databases, or UI layers without touching the core logic.
Scalability: Supports growth and integration without sacrificing structure.
A Practical Example
The following example demonstrates how Clean Architecture principles can be applied in practice, illustrating the separation of concerns across its distinct concentric circles.
Domain Object - Entities
package com.thedevhorse.cleanarchitecture.domain;
public class Athlete {
private String athleteId;
private String name;
private int age;
private Category category;
private Athlete(String athleteId,
String name,
int age) {
this.athleteId = athleteId;
this.name = name;
this.age = age;
setCategory(age);
}
public static Athlete create(final String athleteId,
final String name,
final int age) {
return new Athlete(athleteId, name, age);
}
}
Represents the core business entity. Independent from infrastructure, frameworks, or databases.
Use Case Implementation
package com.thedevhorse.cleanarchitecture.usecase;
public class AthleteUseCaseImpl implements AthleteInputPort {
private final AthleteDaoOutputPort athleteDaoOutputPort;
public AthleteUseCaseImpl(AthleteDaoOutputPort athleteDaoOutputPort) {
this.athleteDaoOutputPort = athleteDaoOutputPort;
}
@Override
public Athlete getAthlete(final String athleteId) {
return athleteDaoOutputPort.getAthleteById(athleteId);
}
@Override
public void createAthlete(final Athlete athlete) {
athleteDaoOutputPort.createAthlete(athlete);
}
@Override
public void updateAthlete(final Athlete athlete) {
athleteDaoOutputPort.updateAthlete(athlete);
}
}
Implements application-specific business rules, orchestrating actions between ports and entities.
Use Case - Input Port
package com.thedevhorse.cleanarchitecture.usecase.port.in;
public interface AthleteInputPort {
Athlete getAthlete(String athleteId);
void createAthlete(Athlete athlete);
void updateAthlete(Athlete athlete);
}
Defines the contract for use cases, shielding the implementation from external dependencies.
Use Case - Output Port
package com.thedevhorse.cleanarchitecture.usecase.port.out;
public interface AthleteDaoOutputPort {
Athlete getAthleteById(String athleteId);
void createAthlete(Athlete athlete);
void updateAthlete(Athlete athlete);
}
Specifies how the use case interacts with persistence layers without knowing the actual database details.
Interface Adapters – Controller
package com.thedevhorse.cleanarchitecture.infra.controller;
@RestController
@RequestMapping("/api/athletes")
public class AthleteController {
private final AthleteInputPort athleteInputPort;
public AthleteController(AthleteInputPort athleteInputPort) {
this.athleteInputPort = athleteInputPort;
}
@GetMapping("/{athleteId}")
public AthleteResponse getAthlete(@PathVariable String athleteId) {
return mapToAthleteResponse(
athleteInputPort.getAthlete(athleteId)
);
}
@PostMapping
public void createAthlete(@RequestBody AthleteRequest athleteRequest) {
athleteInputPort.createAthlete(
mapToAthlete(athleteRequest)
);
}
@PutMapping
public void updateAthlete(@RequestBody AthleteRequest athleteRequest) {
athleteInputPort.updateAthlete(
mapToAthlete(athleteRequest)
);
}
}
Acts as the bridge between HTTP requests and the use case, adapting incoming and outgoing data.
Frameworks & Drivers – DB
package com.thedevhorse.cleanarchitecture.infra.repository;
@Component
public class AthleteDaoImpl implements AthleteDaoOutputPort {
private final AthleteRepository athleteRepository;
public AthleteDaoImpl(AthleteRepository athleteRepository) {
this.athleteRepository = athleteRepository;
}
@Override
public Athlete getAthleteById(final String athleteId) {
return mapToAthlete(findEntityById(athleteId));
}
@Override
public void createAthlete(Athlete athlete) {
athleteRepository.save(mapToAthleteEntity(athlete));
}
@Override
public void updateAthlete(Athlete athlete) {
AthleteEntity athleteEntity = findEntityById(athlete.athleteId());
athleteEntity.setAge(athlete.age());
athleteEntity.setName(athlete.name());
athleteRepository.save(athleteEntity);
}
}
Implements persistence operations using a specific framework and database, hidden behind the output port interface.
Conclusion
By applying Clean Architecture, you build applications where:
- Core business logic is isolated from technical details.
- Responsibilities are clearly separated across layers.
- Frameworks and tools are replaceable without rewriting the business core.
This results in flexible, maintainable, and testable systems — ready to evolve as your requirements change.
Additional Resources
For a step-by-step video walkthrough of this example and further explanation of the pattern in action, watch the full tutorial:
🟥▶️https://www.youtube.com/watch?v=4hVbaHeaJy4
Remember, real speed doesn't come from rushing. It comes from doing things right. As Robert C. Martin said, "The only way to go fast, is to go well."
Top comments (0)