Lean Architecture in Kotlin: From Theory to Code

How Kotlin empowers focused, maintainable software through practical architectural principles—applied across mobile, backend, and multiplatform ecosystems.

In the age of relentless software iteration, where applications stretch across mobile, backend, and web environments, developers are asking deeper questions. How do we build software that adapts to constant change, scales cleanly, and resists the entropy of growing codebases?

The answer isn’t another framework or library—it’s architecture. And more specifically, Lean Architecture, a practical, value-driven approach to software design that prioritizes simplicity, flow, and flexibility over rigid structures.

When paired with Kotlin—a language that prizes expressiveness and clarity—Lean Architecture becomes more than a theoretical model. It becomes a strategic approach to writing maintainable, modular code across platforms, from Android to the server, and even in Kotlin Multiplatform (KMP) projects.

This article explores what Lean Architecture really means in modern Kotlin development, and how to move from conceptual understanding to working code—with tangible examples that reflect real-world needs.

What Is Lean Architecture?

Derived from Lean Manufacturing and championed by software thinkers like Uncle Bob (Robert C. Martin), Lean Architecture isn’t just about “clean code.” It’s about designing systems that:

  • Avoid unnecessary complexity
  • Deliver user value quickly
  • Decouple business logic from delivery mechanisms
  • Enable evolutionary change

Lean Architecture favors high cohesion and low coupling, which leads to better testability, better parallel development, and better adaptability.

Core Principles of Lean Architecture (Applied to Kotlin)

Lean Architecture isn’t prescriptive in technology. Instead, it outlines responsibilities and boundaries. Here’s how those map to Kotlin projects:

1. Separate Concerns

Separate UI, business rules, and infrastructure logic into distinct modules or layers.

  • In Android: decouple UI from ViewModels, usecases, and repositories.
  • In backend: decouple routing from domain logic and persistence.

2. Code-to-an-Interface, Not an Implementation

Use Kotlin interfaces (or abstract classes) to invert dependencies. This allows swappable implementations without entangling layers.

3. Use Dependency Inversion

The core application logic should not depend on details like frameworks or databases. Instead, those should depend on the core.

4. Favor Composition Over Inheritance

Kotlin makes this easy with first-class support for functions as values, extension functions, and sealed classes.

Lean Architecture Layers (Simplified for Kotlin)

Let’s define a clear model that works across all Kotlin platforms:

cssCopyEdit[ UI / Presentation ]
       ↓
[ Application / Usecases ]
       ↓
[ Domain Logic (Core) ]
       ↓
[ Infrastructure / Data / IO ]

Each layer:

  • Only knows about the layer directly below it.
  • Depends on interfaces defined upward, not concrete classes downward.

This results in a Dependency Rule: Source code dependencies point inward, toward the domain.

Step-by-Step: Applying Lean Architecture in Kotlin

Let’s now build a simple app—a Task Manager—across different Kotlin contexts (Android, backend, KMP), applying Lean Architecture every step of the way.

🔹 1. Define the Domain Layer

The domain layer is the heart of your application. It includes core models, logic, and rules—no Android, no SQL, no frameworks.

Task.kt:

kotlinCopyEditdata class Task(
    val id: String,
    val title: String,
    val completed: Boolean
)

TaskRepository.kt:

kotlinCopyEditinterface TaskRepository {
    fun getAll(): List<Task>
    fun save(task: Task)
    fun complete(id: String)
}

CompleteTaskUseCase.kt:

kotlinCopyEditclass CompleteTaskUseCase(private val repository: TaskRepository) {
    fun execute(taskId: String) {
        repository.complete(taskId)
    }
}

📌 Notice: No platform dependencies. This can be reused across Android, server, or even KMP.

🔹 2. Infrastructure Layer (Android + Backend)

This layer provides real implementations of interfaces, like repositories or network clients.

✅ Android Implementation

TaskRepositoryImpl.kt:

kotlinCopyEditclass AndroidTaskRepository : TaskRepository {
    private val tasks = mutableListOf<Task>()

    override fun getAll(): List<Task> = tasks

    override fun save(task: Task) {
        tasks.add(task)
    }

    override fun complete(id: String) {
        tasks.replaceAll {
            if (it.id == id) it.copy(completed = true) else it
        }
    }
}

✅ Server-side (Ktor)

kotlinCopyEditclass InMemoryTaskRepo : TaskRepository {
    private val map = ConcurrentHashMap<String, Task>()

    override fun getAll() = map.values.toList()

    override fun save(task: Task) {
        map[task.id] = task
    }

    override fun complete(id: String) {
        map.computeIfPresent(id) { _, task -> task.copy(completed = true) }
    }
}

🔹 3. Application Layer (Usecases)

This layer orchestrates domain logic for specific actions. Here’s another example:

CreateTaskUseCase.kt:

kotlinCopyEditclass CreateTaskUseCase(private val repo: TaskRepository) {
    fun execute(title: String): Task {
        val task = Task(UUID.randomUUID().toString(), title, false)
        repo.save(task)
        return task
    }
}

🔹 4. Presentation Layer

✅ Android ViewModel

kotlinCopyEditclass TaskViewModel(
    private val createTask: CreateTaskUseCase,
    private val completeTask: CompleteTaskUseCase
) : ViewModel() {

    private val _tasks = MutableLiveData<List<Task>>()
    val tasks: LiveData<List<Task>> = _tasks

    fun loadTasks() {
        _tasks.value = repo.getAll()
    }

    fun addTask(title: String) {
        createTask.execute(title)
        loadTasks()
    }

    fun completeTask(id: String) {
        completeTask.execute(id)
        loadTasks()
    }
}

✅ Ktor Route (Server)

kotlinCopyEditrouting {
    post("/task") {
        val request = call.receive<CreateTaskRequest>()
        val task = createTaskUseCase.execute(request.title)
        call.respond(task)
    }

    post("/task/{id}/complete") {
        val id = call.parameters["id"] ?: return@post call.respond(HttpStatusCode.BadRequest)
        completeTaskUseCase.execute(id)
        call.respond(HttpStatusCode.OK)
    }
}

Making It Kotlin Multiplatform (KMP)

One of Kotlin’s biggest advantages is its ability to reuse business logic across platforms.

  1. Move your domain and usecases into a shared KMP module.
  2. Keep platform-specific implementations in separate targets.
  3. Use expect/actual or interfaces for platform abstraction.

Shared module:

kotlinCopyEditexpect class PlatformLogger() {
    fun log(message: String)
}

Android actual:

kotlinCopyEditactual class PlatformLogger {
    actual fun log(message: String) {
        Log.d("App", message)
    }
}

This structure allows your Lean Architecture to scale across platforms with minimal duplication.

Benefits of Lean Architecture in Kotlin Projects

Testability

With clean separation and dependencies inverted, writing unit tests is straightforward.

kotlinCopyEdit@Test
fun `completing task marks it as complete`() {
    val repo = FakeTaskRepo()
    val usecase = CompleteTaskUseCase(repo)

    val task = Task("1", "Test", false)
    repo.save(task)

    usecase.execute("1")

    assertTrue(repo.getAll().first().completed)
}

Scalability

Need to swap from in-memory to SQLite? Just change the implementation, not the business logic.

Cross-platform Agility

Your core logic remains untouched, whether the app runs on Android, iOS, or a server.

Faster Development

Each layer can be worked on independently. Frontend developers don’t need to wait for backend implementations.

Common Pitfalls to Avoid

❌ Too Many Layers

Lean Architecture is not about abstraction for abstraction’s sake. If you’re not sharing code or swapping implementations, don’t over-engineer.

❌ Improper Dependency Flow

Ensure dependencies point inward—from outer layers to inner (domain). Not the other way around.

❌ Putting Logic in the Wrong Layer

Business logic in ViewModels or UI controllers leads to coupling. Always push it down into the domain or application layers.

Evolving Your Lean Kotlin Stack

You can expand your architecture as the project grows:

  • Use StateFlow or Compose for reactive presentation.
  • Introduce coroutines or Flow for asynchronous data flow.
  • Add interfaces for every boundary to increase testability.
  • Use Dependency Injection (like Koin or Hilt) to wire layers dynamically.

But remember: Lean means just enough. Introduce complexity only when it adds value.

Conclusion

In a world where software must be fast, flexible, and future-proof, Lean Architecture offers Kotlin developers a framework not just for building apps—but for building sustainable systems.

By prioritizing clarity, cohesion, and boundary-driven thinking, you reduce friction, increase agility, and write code that adapts to change rather than breaking under it.

From Android to the backend to full multiplatform apps, Kotlin and Lean Architecture offer a practical, modern foundation for everything we’re building now—and everything still to come.

The tools are ready. The theory is solid. Now it’s your turn to build, lean.

Read:

Kotlin Wasm (WebAssembly): Writing Web Apps in Kotlin Without JS

Kotlin and AI: Building an AI Chatbot Using Kotlin + OpenAI API

Spring Boot with Kotlin: Boost Your Productivity in Web Development

Kotlin Native: Building a CLI Tool Without JVM


FAQs

1. What is Lean Architecture in software development, and how is it applied in Kotlin?

Lean Architecture is a software design philosophy focused on simplicity, testability, and separation of concerns. In Kotlin, it involves organizing code into clean layers—UI, application/use cases, domain, and infrastructure—with each layer having clearly defined responsibilities and depending only on abstractions from inner layers. It helps make codebases scalable, maintainable, and easier to test.

2. How does Lean Architecture differ from Clean Architecture or MVVM?

Lean Architecture shares many principles with Clean Architecture, such as dependency inversion and layered separation, but emphasizes pragmatism and simplicity. Unlike MVVM, which primarily focuses on UI design patterns, Lean Architecture organizes the entire application structure, including business logic and infrastructure. It avoids over-engineering and focuses on just enough abstraction.

3. Can Lean Architecture be used in Kotlin Multiplatform (KMP) projects?

Yes, Lean Architecture is an excellent fit for Kotlin Multiplatform (KMP). By isolating the domain and application layers in a shared module, you can reuse core business logic across Android, iOS, server, and web targets, while keeping platform-specific code (like UI or networking) in separate modules.

4. What are the key benefits of using Lean Architecture in Kotlin projects?

Lean Architecture in Kotlin offers several benefits:

  • Improved testability due to decoupled layers
  • Reusability across Android, backend, and KMP
  • Faster iteration thanks to modular code
  • Easier maintenance as features grow
  • Platform independence for domain logic

It enables teams to scale their applications without increasing complexity.

5. Do I need special frameworks or libraries to implement Lean Architecture in Kotlin?

No. Lean Architecture is not tied to any framework. You can implement it using plain Kotlin, leveraging features like interfaces, data classes, coroutines, and sealed classes. However, tools like Koin, Hilt, or Ktor can complement the architecture for dependency injection or server-side support if needed.

Leave a Comment