Learn through the super-clean Baeldung Pro experience:
>> Membership and Baeldung Pro.
No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.
Last updated: March 19, 2024
In this tutorial, we’ll learn the concept of clean architecture using the Kotlin language. Clean architecture simply refers to a pattern with the dependency rule that the inner layer shouldn’t be concerned about the outer layers. Code written in an inner layer can’t directly reference any code from the outer layer. On the contrary, an outer layer can access code written in the inner layers.
Let’s first review a few advantages of clean architecture:
Now, let’s look at a couple of disadvantages of implementing clean architecture:
In this step, we’ll discuss the various packages/layers we can use to apply clean architecture to our project. These packages include:
Entities are simply the core business objects that we use to represent our application’s domain and are completely independent of any external framework or database we’re using.
In Kotlin, we usually create entities as data classes:
data class User(val id: Long, val username: String, val email: String)
Use cases, which are also referred to as interactions, are used to represent our application-specific business rules and logic. Use cases usually encapsulate the core functionality of our application.
As we add more features to our backend, the number of use cases increases, too. It’s always good to adhere to the rule that each use case should be a standalone Kotlin class or function responsible for performing a specific action.
Let’s take a look at our use case example:
class CreateUserUseCase(private val userRepository: UserRepository) {
fun execute(username: String, email: String): User {
return userRepository.createUser(username, email)
}
}
Interfaces – also referred to as protocols – are used to define abstract functions that interact with other external systems such as databases and web services. We define interfaces in the innermost layer of our project and implement them in the outer layers. This allows us to replace or change external dependencies without affecting our core business logic.
Let’s write an interface for our example use case:
interface UserRepository {
fun createUser(username: String, email: String): User
}
The frameworks layer implements the interfaces we defined in our project’s core layer. This includes implementing, for example, our database and web APIs using libraries such as Ktor.
Let’s see an example of code using Ktor to create a basic controller for handling HTTP requests and responses:
routing {
route("/users") {
get {
call.respond(users)
}
post {
val newUser = call.receive<User>()
users.add(newUser)
call.respond(HttpStatusCode.Created, newUser)
}
get("/{username}") {
val username = call.parameters["username"]
val user = users.find { it.username == username }
if (user != null) {
call.respond(user)
} else {
throw NotFoundException("User not found")
}
}
}
}
Adapters are the components that bridge the gap between the business logic and other outer layers that deal with databases, web frameworks, or interfaces. Their purpose is to allow us to translate data and actions from one form to another, ensuring that our inner core remains independent of external technologies and frameworks. There are two types of adapters we’re going to discuss.
For example, let’s write an adapter that is responsible for interacting with our improvised database:
class DatabaseUserRepository : UserRepository {
private val users = mutableMapOf<Long, User>()
private var idCounter = 1L
override fun createUser(username: String, email: String): User {
val newUser = User(idCounter++, username, email)
users[newUser.id] = newUser
return newUser
}
}
Input Adapters handle external inputs to the application, such as HTTP requests, user interface interactions, or messages from a queue. They normally act as a bridge between the external world and the core business logic. Input adapters are typically designed to be lightweight and focused on converting external data into a format that the core application can understand. Essentially, they are often involved in parsing and validating our incoming data and then passing it to the appropriate use case or application service.
Input adapters are often associated with frameworks like Ktor and Spring Boot.
Output Adapters are responsible for interacting with external systems and frameworks, such as databases, external APIs, message queues, or file systems. They encapsulate the details of how our data is stored, retrieved, or communicated with external services.
Output adapters ensure that our core business logic remains agnostic to the specific details of the external systems it interacts with. Normally, output adapters often involve database operations, API calls, or formatting and sending messages to external systems.
In this article, we went through a definition of clean architecture and learned how it can benefit us as we create our projects using pure Kotlin. Along the way, we also looked at the layers/packages that are associated with clean architecture and saw some quick Kotlin examples to illustrate each one.