1. Overview

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.

2. Advantages of Clean Architecture

Let’s first review a few advantages of clean architecture:

  • Enhanced maintainability: Clean Architecture ensures that changes to one part of the system do not ripple through the entire codebase, making maintenance more efficient and reducing the risk of unintended side effects.
  • The clear separation of business logic from other architectural layers simplifies the process of adding or updating features within the codebase.
  • The code’s modularity and decoupling enable us to treat each layer as an independent entity during testing.

3. Disadvantages of Clean Architecture

Now, let’s look at a couple of disadvantages of implementing clean architecture:

  • Requires extra effort that might not be worth it for relatively simple projects
  • Steep learning curve

4. Layers in 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
  • Use Cases
  • Interfaces
  • Frameworks
  • Adapters

4.1. Entities

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)

4.2. Use Cases

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)
    }
}

4.3. Interfaces

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
}

4.4. Frameworks

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")
            }
        }
    }
}

4.5. Adapters

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.

5. Conclusion

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.

Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.