Origins and Evolution of Clean Architecture
The concept of Clean Architecture was formalized by Robert C. Martin (“Uncle Bob”) in 2012. It was not a discovery of new principles, but a synthesis of several existing architectural patterns designed to address the increasing entanglement of business logic with framework-specific code.
1. The Precursor Patterns
Clean Architecture integrates ideas from four primary architectural movements:
- Hexagonal Architecture (Ports and Adapters): Developed by Alistair Cockburn around 2005. It introduced the idea that an application should be equally driven by users, programs, automated tests, or batch scripts, and be developed and tested in isolation from its eventual run-time devices and databases.

Above image is extracted from a blog on about Hexagonal Architecture and Clean Architecture (with examples) by Dyarlen Iber
- Onion Architecture: Proposed by Jeffrey Palermo in 2008. This pattern placed the Domain Model at the center and established the rule that all coupling is toward the center. It emphasized that the application core should not depend on the database or any infrastructure concerns.

Above image is extracted from a medium blog on Onion Architecture by Ritesh Kapoor
- Screaming Architecture: A concept emphasizing that the folder structure and organization of a project should communicate the intent of the system (e.g., “Healthcare System”) rather than the frameworks used (e.g., “Ruby on Rails” or “Spring Boot”).
- DCI (Data, Context, and Interaction) and BCE (Boundary-Control-Entity): These patterns contributed to the definition of how objects interact within use cases and how boundaries between the system and the external world are managed.
2. The Synthesis (2012)
Robert C. Martin unified these concepts into a single actionable framework called “Clean Architecture.” The goal was to provide a standardized approach to building systems that are:
- Independent of Frameworks: The architecture does not rely on the existence of some library of feature-laden software. This allows you to use frameworks as tools, rather than cramming your system into their limited constraints.
- Testable: The business rules can be tested without the UI, Database, Web Server, or any other external element.
- Independent of UI: The UI can change easily, without changing the rest of the system. A Web UI could be replaced with a console UI, for example, without changing the business rules.
- Independent of Database: You can swap out SQL Server or Oracle for MongoDB, BigTable, CouchDB, or something else. Your business rules are not bound to the database.
3. The Shift Toward Domain-Driven Design (DDD)
The evolution of Clean Architecture is closely tied to the rise of Domain-Driven Design (DDD). It adopted the DDD emphasis on the “Domain” as the most important part of the software. This led to the structural requirement that the innermost circle—the Entities—contains the most general and high-level rules, while the outer circles contain the mechanisms (UI, DB, etc.). This ensures that the most stable part of the application (the business logic) does not depend on the most volatile parts (the technology stack).
The Core Reasoning
The fundamental objective of Clean Architecture is the isolation of business logic from technical implementation details. This separation ensures that the software remains adaptable, maintainable, and testable throughout its lifecycle.
1. Separation of Concerns and Framework Independence
Software systems often become rigid when business rules are tightly coupled to specific frameworks (e.g., Spring, Express, .NET) or libraries. Clean Architecture treats these as “details.” By decoupling the core logic, the organization can upgrade or replace frameworks without rewriting the underlying business rules. This protects the enterprise’s intellectual property from the volatility of the technology market.
2. Protection of the Domain (Domain Integrity)
The most critical reasoning behind this architecture is the preservation of the “Domain.” The domain contains the rules that define how the business functions.
To ensure the domain remains valid and consistent, Clean Architecture utilizes Value Objects. Unlike Entities, which have a unique identity, Value Objects are defined by their attributes. Their core reasoning includes:
- Self-Validation: A Value Object is responsible for its own validity. It cannot be instantiated in an invalid state. For example, an
Emailvalue object will validate the format upon creation. - Immediate Error Handling: If the input data fails business constraints, the Value Object throws a domain-specific error (e.g.,
InvalidEmailException). This prevents “garbage data” from propagating into the deeper layers of the application. - Immutability: Once created, a Value Object cannot be changed. Any modification results in a new instance. This eliminates side effects and makes the system’s behavior predictable.
3. Testability Without Side Effects
When business logic is isolated, unit testing does not require a database connection, a web server, or any external infrastructure. Tests run faster and are more reliable because they focus strictly on logic. By using Value Objects that validate themselves, the testing suite can verify that the system correctly rejects invalid data at the boundary of the domain layer, ensuring the “Golden Rule” of the domain is never violated.
4. Dependency Inversion
The reasoning for the Dependency Rule (dependencies point only inward) is to ensure that the high-level policy (Business Rules) does not depend on low-level detail (Data Access, UI).
- Stable Abstractions: The inner layers define interfaces (Ports).
- Volatile Implementations: The outer layers implement these interfaces (Adapters).
This inversion allows developers to defer decisions about databases or external APIs until they are absolutely necessary, or change them later with minimal impact on the core code.
5. Long-term Velocity
While Clean Architecture requires more boilerplate and initial setup (due to the creation of DTOs, Mappers, and Value Objects), the reasoning is to prevent “Software Rot.” By maintaining strict boundaries, the cost of adding new features or changing existing ones remains relatively constant over time, rather than increasing exponentially as the codebase grows.
Architectural Layers
The architecture is organized into four concentric layers, each representing a different level of abstraction and isolation.
1. Entities (The Domain Layer)
This is the innermost circle, representing the core business rules of the enterprise. It contains the most general and high-level rules that are least likely to change when external factors shift.
- Entities: Objects that have a unique identity and a lifecycle. They encapsulate the fundamental business logic of the system.
- Value Objects: These represent descriptive aspects of the domain with no conceptual identity. They are defined by their attributes and are immutable.
- Validation and Error Handling: Value Objects are responsible for enforcing domain invariants. They validate their own state during instantiation.
- Logic Enforcement: If a
PasswordValue Object requires a minimum of eight characters, the constructor must validate this constraint. If the input is invalid, it must immediately throw a domain-specific exception (e.g.,WeakPasswordException). This ensures that no invalid data can ever penetrate the domain layer. - Independence: This layer must have zero dependencies on any external library, framework, or database driver.
2. Use Cases (The Application Layer)
This layer contains application-specific business rules. it orchestrates the flow of data to and from the entities and directs those entities to use their enterprise-wide business rules to achieve the goals of the use case.
- Functionality: It implements all the “features” of the system (e.g.,
ProcessOrder,RegisterUser,GenerateReport). - Decoupling: Use cases do not know how data is stored or how it is presented to the user. They interact with the outer layers through interfaces (Input and Output Ports).
- Boundary Crossing: This layer defines the contracts (interfaces) that the infrastructure layer must implement.
3. Interface Adapters
This layer is a set of adapters that convert data from the format most convenient for the use cases and entities to the format most convenient for some external agency like the Database or the Web.
- Controllers: Convert incoming requests (e.g., HTTP requests) into simple data structures that the use cases can understand.
- Presenters: Convert the results from a use case back into a format suitable for the UI or API response.
- Gateways: Interfaces used by the application layer to communicate with external systems (like a database or an external mail service).
- Mappers: This layer contains the logic to map Domain Entities/Value Objects into Data Transfer Objects (DTOs) or Persistence Models, and vice-versa.
4. Frameworks and Drivers (The Infrastructure Layer)
The outermost layer is composed of frameworks and tools such as the Database, the Web Framework, the GUI, and the configuration.
- External Mechanisms: This is where the actual implementation of the database (e.g., SQL scripts, MongoDB clients) and the web server (e.g., Express, Spring Boot controllers) resides.
- Volatility: This layer is the most volatile. Because it is kept separate from the inner layers, replacing a database or switching from a REST API to a message queue involves changing code only in this layer.
- Dependency: This layer depends on the inner layers by implementing the interfaces defined in the Use Case or Domain layers. It is the “plug-in” area of the architecture.
Proposed File Structure (Layer-Wise)
This structure organizes the project strictly by architectural layers. It separates technical concerns across the entire application, which is suitable for smaller to mid-sized projects where a feature-based split is not yet required.
src/
├── domain/ # Enterprise-wide business logic and rules
│ ├── entities/ # Core objects with identity (e.g., User.ts, Order.ts)
│ ├── value-objects/ # Immutable objects with self-validation logic
│ │ ├── Email.ts # Validates format; throws InvalidEmailError
│ │ ├── Password.ts # Validates strength; throws WeakPasswordError
│ │ └── Money.ts # Validates non-negative; throws InvalidAmountError
│ ├── repositories/ # Interfaces/Contracts for data persistence
│ ├── exceptions/ # Domain-specific error classes
│ └── services/ # Domain services for logic involving multiple entities
│
├── application/ # Application-specific business rules (Use Cases)
│ ├── use-cases/ # Orchestrators (e.g., CreateUser.ts, GetOrder.ts)
│ ├── ports/ # Interfaces for external services (e.g., IMailer.ts)
│ └── dtos/ # Data Transfer Objects for Use Case input/output
│
├── infrastructure/ # External implementations and technical details
│ ├── persistence/ # Database-specific logic
│ │ ├── models/ # Database schemas or ORM entities
│ │ ├── repositories/ # Concrete implementations of Domain repositories
│ │ └── mappers/ # Maps Persistence Models <-> Domain Entities
│ ├── external-services/ # Concrete implementations of Application ports
│ │ ├── mailer/ # e.g., SendGrid or SMTP implementation
│ │ └── storage/ # e.g., AWS S3 or LocalStorage implementation
│ └── config/ # Environment and framework-specific configurations
│
├── presentation/ # Entry points and delivery mechanisms
│ ├── http/ # REST/GraphQL API implementation
│ │ ├── controllers/ # Handles requests and invokes Use Cases
│ │ ├── middlewares/ # Auth, validation, and error handling
│ │ └── routes/ # Endpoint definitions
│ ├── cli/ # Terminal/Command-line interface logic
│ └── view-models/ # Formatting logic for the final response/UI
│
└── main/ # Composition Root
├── di/ # Dependency Injection wiring and containers
├── factories/ # Logic for assembling complex object graphs
└── index.ts # Application entry point
Layer-Wise Component Details
Domain Layer: Value Objects and Validation
In this layer-based approach, all Value Objects reside in domain/value-objects/.
- Validation Logic: Every Value Object contains a private constructor and a static
createmethod (or logic within the constructor) that performs validation. - Error Handling: If an input violates a business rule (e.g., a
ZipCodethat is not 5 digits), the Value Object throws a specific exception fromdomain/exceptions/. This ensures that an invalid object can never exist within theapplicationorinfrastructurelayers.
Application Layer: Use Case Orchestration
The application/use-cases/ directory contains the logic that bridges the UI and the Domain.
- Process: It receives a DTO from the
presentationlayer, uses thedomain/value-objects/to validate the data, interacts withdomain/repositories/(interfaces), and returns a result. - Isolation: This layer knows “what” needs to happen but has no knowledge of “how” the data is stored or “how” the response is rendered.
Infrastructure Layer: The Implementation Detail
The infrastructure/ directory contains the “low-level” code.
- Mappers: Crucial in this structure to prevent database-specific logic from leaking.
infrastructure/persistence/mappers/convert raw database rows into the richDomain EntitiesandValue Objectsdefined in the center. - Adapters: If the application needs to send an email, the concrete implementation (e.g.,
NodemailerAdapter) stays here, satisfying the interface defined inapplication/ports/.
Presentation Layer: Data Transformation
This layer is responsible for the “delivery” of the application.
- Controllers: In
presentation/http/controllers/, the code handles the web-specific details like HTTP status codes and headers. It maps the incoming request body into an Application DTO. - Error Mapping: This layer is also responsible for catching the domain-specific exceptions (thrown by Value Objects) and converting them into user-friendly error messages or appropriate HTTP status codes (e.g., 400 Bad Request).
Native Android (Kotlin) Clean Architecture File Structure
In standard Android development, Clean Architecture is typically organized into three primary layers: Domain (pure Kotlin), Data (Infrastructure), and Presentation (Android UI). These can be implemented as separate Gradle modules or as packages within a single app module.
Below is the package structure for a single-module Android application utilizing modern components like Jetpack Compose, ViewModels, Room, and Retrofit.
app/src/main/java/com/example/app/
├── domain/ # Pure Kotlin/Java (No Android dependencies)
│ ├── model/ # Entities and Value Objects
│ │ ├── User.kt # Entity with identity
│ │ └── valueobject/
│ │ ├── Email.kt # Kotlin class/value class with validation
│ │ ├── Password.kt # Validates rules and throws Domain Exceptions
│ │ └── Money.kt
│ ├── repository/ # Interface definitions for data operations
│ │ └── UserRepository.kt
│ ├── usecase/ # Single-purpose business rules
│ │ ├── CreateUserUseCase.kt
│ │ └── GetUserUseCase.kt
│ └── exception/ # Custom Domain Exceptions
│ └── DomainExceptions.kt # e.g., InvalidEmailException, WeakPasswordException
│
├── data/ # Data management and Infrastructure
│ ├── repository/ # Implementations of Domain repositories
│ │ └── UserRepositoryImpl.kt
│ ├── mapper/ # Maps Data Entities <-> Domain Models
│ │ └── UserMapper.kt
│ ├── local/ # Room Database, SQLite, DataStore
│ │ ├── database/ AppDatabase.kt
│ │ ├── dao/ UserDao.kt
│ │ └── entity/ UserEntity.kt # Room Database Entity
│ └── remote/ # Retrofit, Ktor, API Clients
│ ├── api/ UserApiService.kt
│ └── dto/ UserResponseDto.kt # Network response objects
│
├── presentation/ # Android UI and ViewModels
│ ├── common/ # Reusable UI components, themes
│ ├── user/ # Grouped by screen/flow
│ │ ├── UserViewModel.kt # Orchestrates Use Cases and manages UI state
│ │ ├── UserUiState.kt # Data class representing the screen state
│ │ └── UserScreen.kt # Jetpack Compose UI (or Fragment/Activity)
│ └── mapper/ # UI Mappers (Domain -> UI State)
│
└── di/ # Dependency Injection (Hilt, Koin, or Dagger)
└── AppModule.kt # Provides DB, API, and Repository singletons
Domain Layer: Code Samples (Kotlin/Android)
The Domain Layer is pure Kotlin and contains zero dependencies on Android frameworks, databases, or network libraries.
1. Custom Domain Exceptions
Define specific exceptions to represent business rule violations.
sealed class DomainException(message: String) : Exception(message)
class InvalidEmailException(email: String) :
DomainException("The provided email '$email' is not a valid format.")
class WeakPasswordException(reason: String) :
DomainException("Password does not meet security requirements: $reason")
class NegativeAmountException :
DomainException("Monetary amounts cannot be negative.")
2. Value Objects with Validation
Value Objects encapsulate validation logic. They are immutable and ensure that the system never processes “garbage” data.
@JvmInline
value class Email(val value: String) {
init {
val emailRegex = "^[A-Za-z0-9+_.-]+@(.+)$".toRegex()
if (!value.matches(emailRegex)) {
throw InvalidEmailException(value)
}
}
}
data class Password(val value: String) {
init {
if (value.length < 8) {
throw WeakPasswordException("Must be at least 8 characters.")
}
if (!value.any { it.isDigit() }) {
throw WeakPasswordException("Must contain at least one digit.")
}
}
}
3. Entities
Entities have a unique identity (ID) and encapsulate enterprise-wide business rules.
data class User(
val id: String, // Unique Identity
val email: Email, // Value Object
val password: Password, // Value Object
val isActive: Boolean = true
) {
// Business logic within the entity
fun deactivate(): User {
return this.copy(isActive = false)
}
}
4. Repository Interfaces (Ports)
These interfaces define the contracts for data operations. The implementation resides in the Infrastructure/Data layer.
interface UserRepository {
suspend fun save(user: User)
suspend fun findByEmail(email: Email): User?
suspend fun exists(email: Email): Boolean
}
5. Use Cases (Application-Specific Rules)
Use Cases orchestrate the flow of data. They use the Repository interfaces and Domain Entities to perform a specific task.
class RegisterUserUseCase(
private val userRepository: UserRepository
) {
suspend operator fun invoke(emailInput: String, passwordInput: String) {
// 1. Validation happens automatically during Value Object instantiation
val email = Email(emailInput)
val password = Password(passwordInput)
// 2. Business Rule Check
if (userRepository.exists(email)) {
throw DomainException("A user with this email already exists.")
}
// 3. Entity Creation
val newUser = User(
id = java.util.UUID.randomUUID().toString(),
email = email,
password = password
)
// 4. Persistence via Interface
userRepository.save(newUser)
}
}
Key Implementation Principles in this Layer
- Validation at the Gate: The
EmailandPasswordobjects cannot be created in an invalid state. If theRegisterUserUseCasereceives bad strings, the code crashes (with a caught DomainException) before any business logic is executed. - No Frameworks: Notice the absence of
Context,Parcelable,JSONObject, or Room annotations (@Entity,@PrimaryKey). This ensures the core logic is 100% unit-testable on a standard JVM without an emulator. - Immutability: Data classes and
valueclasses are used to ensure that once a domain object is validated and created, its state cannot be modified unpredictably.
Data Layer: Code Samples (Kotlin/Android with API)
The Data Layer (Infrastructure) implements the interfaces defined in the Domain Layer. It handles the technical details of communicating with external APIs using libraries like Retrofit and mapping external data formats into Domain Entities and Value Objects.
1. Data Transfer Objects (DTOs)
These classes represent the structure of the JSON data returned by the API. They are annotated with serialization metadata and remain separate from Domain Entities.
import com.google.gson.annotations.SerializedName
data class UserResponseDto(
@SerializedName("id") val id: String,
@SerializedName("email_address") val email: String,
@SerializedName("is_active") val isActive: Boolean,
@SerializedName("created_at") val createdAt: String
)
data class UserRequestDto(
@SerializedName("email") val email: String,
@SerializedName("password_hash") val passwordHash: String
)
2. Retrofit API Service
This interface defines the HTTP endpoints. It uses the DTOs for requests and responses.
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Path
interface UserApiService {
@GET("users/{id}")
suspend fun getUserById(@Path("id") id: String): UserResponseDto
@POST("users/register")
suspend fun registerUser(@Body request: UserRequestDto): UserResponseDto
}
3. Data Mappers
Mappers are responsible for converting DTOs into Domain Entities. During this conversion, strings from the API are passed into Value Objects, which triggers the domain-level validation.
// Extension functions for mapping
fun UserResponseDto.toDomain(): User {
return User(
id = this.id,
// Passing raw API string into the Value Object constructor
// If the API returns an invalid email, the Value Object throws an exception here
email = Email(this.email),
password = Password("dummy_api_password"), // Passwords typically aren't returned by APIs
isActive = this.isActive
)
}
fun User.toRequestDto(): UserRequestDto {
return UserRequestDto(
email = this.email.value,
passwordHash = this.password.value // Simplified for example
)
}
4. Repository Implementation
This class implements the UserRepository interface from the Domain Layer. It uses the UserApiService to fetch data and the mappers to return domain objects to the application layer.
class UserRepositoryImpl(
private val apiService: UserApiService
) : UserRepository {
override suspend fun findByEmail(email: Email): User? {
return try {
// Note: In a real scenario, you would have an endpoint to find by email
val response = apiService.getUserById(email.value)
response.toDomain()
} catch (e: Exception) {
// Log error or handle network-specific exceptions
null
}
}
override suspend fun save(user: User) {
val requestDto = user.toRequestDto()
apiService.registerUser(requestDto)
}
override suspend fun exists(email: Email): Boolean {
return try {
apiService.getUserById(email.value)
true
} catch (e: Exception) {
false
}
}
}
Key Implementation Principles in this Layer
- Decoupling: The
UserResponseDtocan change its field names (e.g., fromemail_addresstocontact_email) without affecting theUserentity in the Domain Layer. Only the Mapper needs to be updated. - Infrastructure Isolation: Retrofit annotations and JSON logic are confined to this layer. The Domain Layer remains unaware of the networking library being used.
- Boundary Validation: When
response.toDomain()is called, theEmail(this.email)constructor is invoked. If the server sends an invalid email format, the system throws aDomainExceptionat the boundary of the Data Layer, preventing invalid data from entering the application core. - Dependency Inversion: This layer depends on the
UserRepositoryinterface (Domain) to know what methods to implement, but the Domain does not depend on this implementation.
Presentation Layer: Code Samples (Kotlin/Android)
The Presentation Layer is responsible for rendering data to the screen and capturing user interactions. In modern Android development, this involves a ViewModel to manage state and a UI Component (Jetpack Compose or Fragments) to display it.
1. UI State Representation
The UI state is a data class or sealed class that represents exactly what the user sees on the screen at any given moment.
sealed class RegistrationUiState {
object Idle : RegistrationUiState()
object Loading : RegistrationUiState()
object Success : RegistrationUiState()
data class Error(val message: String) : RegistrationUiState()
}
2. ViewModel Implementation
The ViewModel interacts with the Use Cases. It is responsible for catching DomainException thrown by Value Objects or Use Cases and converting them into human-readable error messages for the UI.
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
class RegistrationViewModel(
private val registerUserUseCase: RegisterUserUseCase
) : ViewModel() {
private val _uiState = MutableStateFlow<RegistrationUiState>(RegistrationUiState.Idle)
val uiState: StateFlow<RegistrationUiState> = _uiState
fun onRegisterClicked(emailInput: String, passwordInput: String) {
viewModelScope.launch {
_uiState.value = RegistrationUiState.Loading
try {
// The Use Case is called with raw strings.
// Internally, the Use Case creates Value Objects (Email, Password).
// If validation fails, a DomainException is thrown immediately.
registerUserUseCase(emailInput, passwordInput)
_uiState.value = RegistrationUiState.Success
} catch (e: DomainException) {
// Catch specific domain errors and update the UI state
_uiState.value = RegistrationUiState.Error(e.message ?: "Invalid Input")
} catch (e: Exception) {
// Catch unexpected system/network errors
_uiState.value = RegistrationUiState.Error("An unexpected error occurred")
}
}
}
}
3. UI Component (Jetpack Compose)
The UI observes the uiState from the ViewModel and reacts to changes. It does not contain any business logic or validation logic; it simply passes strings to the ViewModel.
@Composable
fun RegistrationScreen(viewModel: RegistrationViewModel) {
val state by viewModel.uiState.collectAsState()
var email by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
Column {
TextField(
value = email,
onValueChange = { email = it },
label = { Text("Email") }
)
TextField(
value = password,
onValueChange = { password = it },
label = { Text("Password") },
visualTransformation = PasswordVisualTransformation()
)
Button(onClick = { viewModel.onRegisterClicked(email, password) }) {
Text("Register")
}
// Display state-specific UI
when (state) {
is RegistrationUiState.Loading -> CircularProgressIndicator()
is RegistrationUiState.Success -> Text("Account Created Successfully!", color = Color.Green)
is RegistrationUiState.Error -> Text("Error: ${(state as RegistrationUiState.Error).message}", color = Color.Red)
else -> {}
}
}
}
Key Implementation Principles in this Layer
- Logic-Free Views: The
RegistrationScreendoes not check if the email is valid. It delegates that responsibility to the Domain layer (via the ViewModel and Use Case). This ensures that validation rules are never duplicated in the UI code. - State-Driven UI: The UI is a “dumb” reflection of the
RegistrationUiState. This makes the UI highly predictable and easy to test with Compose UI tests. - Exception Translation: The ViewModel acts as a translator. It catches technical or domain-specific exceptions (like
WeakPasswordException) and transforms them into a state that the UI can render (an error message string). - Decoupling from Domain: The UI layer depends on the ViewModel, and the ViewModel depends on the Use Case interface. The UI never interacts directly with the
UserRepositoryor theUserApiService.
