Book summary · Software architecture · Kotlin
Clean Architecture, Chapter by Chapter: A Kotlin Companion to Uncle Bob’s Guide
Clean Architecture: A Craftsman’s Guide to Software Structure and Design by Robert C. Martin asks one question at great length: how do you structure software so that it stays cheap to change? This post walks through the book part by part and chapter by chapter, in my own words, with diagrams and Kotlin examples. It also covers Martin’s 2012 article “The Clean Architecture”, which is where the famous circle diagram comes from.
- Software has two values: what it does and how easy it is to change. Architecture protects the second one.
- Good architecture keeps business rules in the middle and pushes details (database, web, frameworks) to the edge.
- The Dependency Rule: source code may only point inward, toward higher-level policy.
- Draw boundaries between things that change for different reasons, and cross them with interfaces.
- Group classes into components that change together, avoid cycles, and depend toward stability.
- The database, the web and the framework are details. Put off those decisions as long as you can.
- Do not over-build: the cost of a boundary is real, so add them where change is likely.
Contents
- About the book and where to get it
- Part I: Introduction (Chapters 1 and 2)
- Part II: Programming paradigms (Chapters 3 to 6)
- Part III: Design principles (Chapters 7 to 11)
- Part IV: Component principles (Chapters 12 to 14)
- Part V: Architecture (Chapters 15 to 29)
- The 2012 article: The Clean Architecture
- Part VI: Details (Chapters 30 to 34)
- Appendix: Architecture archaeology
- Putting it together: a small example in Kotlin
- Where I use judgement
- Takeaways, checklist and references
1. About the book and where to get it
Published by Pearson (Prentice Hall) in 2017, the book is about 430 pages in seven parts: an introduction, three parts on principles (programming paradigms, design principles, component principles), a long part on architecture itself, a part on “details”, and an appendix. Its code samples are mostly Java, C and C++. It is a companion to the author’s earlier Clean Code, which I summarised in another post: that book is about code, and this one is about structure.
| Detail | Value |
|---|---|
| Title | Clean Architecture: A Craftsman’s Guide to Software Structure and Design |
| Author | Robert C. Martin |
| ISBN-10 / ISBN-13 | 0134494164 / 978-0134494166 |
| Buy the book | Amazon · InformIT (publisher’s store) |
| Free article | The Clean Architecture (2012), by the same author |
About “downloading”: the book is under copyright, so the official way to get it is to buy it from the links above. I do not link to free copies. The 2012 article is free to read on the author’s blog.
This is a summary in my own words, with my own Kotlin examples, not a replacement for the book. The chapters are short and there is a lot of history and storytelling that a summary cannot carry.
2. Part I: Introduction
Chapter 1: What is design and architecture?
The author argues that there is no real difference between the two. Architecture is the shape of the system, and design is the details of that shape; both are one continuum of decisions from the highest level to the lowest. The goal of both is the same: minimise the human effort needed to build and maintain the system.
The chapter shows a case study of a company whose engineering staff grew many times over while the productivity of each new engineer fell. Each release took more people and delivered less, and the cost per line of code climbed. The cause was a mess that nobody had time to clean, caused by the belief that you can go fast by going sloppy. The author’s reply is blunt: the only way to go fast is to go well.
Chapter 2: A tale of two values
Every piece of software offers two values. Behaviour is what it does for users. Structure is how easily it can change. Business people tend to push for behaviour, and developers are often too shy to defend structure. The author uses the urgent and important grid to explain why this is a mistake: architecture is important but rarely urgent, so it gets pushed aside by features that feel urgent but are often not very important.
The author’s argument: if the software works but is impossible to change, it will not work tomorrow when the requirements change. If it does not work but is easy to change, you can make it work. So it is the developer’s job to fight for the architecture, as part of the job and not as a favour.
3. Part II: Programming paradigms
Chapter 3: Paradigm overview
There are three major paradigms, and the interesting fact is that each one takes something away from the programmer. Structured programming removes unrestricted jumps (goto). Object-oriented programming removes indirect control transfer through function pointers, by making it disciplined. Functional programming removes assignment. None added a new power; each added a discipline. These three restrictions are the base on which architecture sits.
Chapter 4: Structured programming
Edsger Dijkstra showed that unrestricted jumps made programs hard to reason about, and that every program can be built from three structures: sequence, selection (if) and iteration (loops). These can be broken down into provable pieces. In practice we do not prove programs mathematically; instead we test. But tests can only show the presence of bugs, not their absence, much like experiments in science that can only disprove a theory. The architectural takeaway is functional decomposition: break big things into small, testable pieces.
Chapter 5: Object-oriented programming
The author skips the usual slogans (encapsulation, inheritance) and finds the real value in polymorphism, because it gives you control over the direction of source-code dependencies. Normally, a caller depends on what it calls. With an interface, the caller depends on an abstraction and the implementation depends on the same abstraction, so the dependency arrow can point against the flow of control. This is called dependency inversion, and it allows plugin architecture: high-level policy can stay unaware of low-level details.
// The flow of control goes from the policy to the device, but the
// source dependency points the other way: the device implements the policy's interface.
interface Notifier { fun send(message: String) } // owned by the high-level code
class OrderPolicy(private val notifier: Notifier) {
fun complete(orderId: String) = notifier.send("Order $orderId is complete")
}
class EmailNotifier : Notifier { // low-level detail, depends inward
override fun send(message: String) { /* talk to the mail server */ }
}
Chapter 6: Functional programming
Functional programming removes assignment. Variables never change, so there are no race conditions, deadlocks or concurrent-update bugs. Real systems cannot be fully immutable, so the architectural lesson is to separate the parts of the system that mutate from those that do not, and to protect the mutable parts with strict rules (such as transactions). The chapter also introduces event sourcing: instead of storing the current state, store the list of changes and compute the state when needed, just as a bank keeps transactions, not only balances.
sealed interface AccountEvent
data class Deposited(val amount: Int) : AccountEvent
data class Withdrew(val amount: Int) : AccountEvent
// The balance is never stored or mutated; it is derived from the history.
fun balance(events: List<AccountEvent>): Int = events.fold(0) { total, event ->
when (event) {
is Deposited -> total + event.amount
is Withdrew -> total - event.amount
}
}
4. Part III: Design principles
The SOLID principles are the building blocks for the middle layer of the book: how to arrange functions and data into classes, and how to join classes together. I wrote a longer post with runnable exercises on them (Designing for Change), so here I only note what the book adds.
Chapter 7: SRP, the Single Responsibility Principle
The book corrects a common misreading. SRP does not mean “a function does one thing”. It means a module should be responsible to one, and only one, actor: one group of people who ask for changes. In the book’s example, an Employee class has calculatePay() (used by finance), reportHours() (used by operations) and save() (used by the database team). Two of those methods share a helper, and when one team changes the helper, the other team’s feature silently breaks.
// Bad: three actors share one class (and a hidden shared helper)
class Employee {
fun calculatePay(): Money = /* uses regularHours() */ TODO()
fun reportHours(): Hours = /* also uses regularHours() */ TODO()
fun save() { /* database */ }
}
// Clean: separate classes per actor, with plain data in the middle
data class EmployeeData(val id: String, val rate: Money)
class PayCalculator { fun calculatePay(e: EmployeeData): Money = TODO() }
class HourReporter { fun reportHours(e: EmployeeData): Hours = TODO() }
class EmployeeSaver { fun save(e: EmployeeData) { /* database */ } }
Chapter 8: OCP, the Open-Closed Principle
A system should be easy to extend without modifying existing code. The chapter applies this at architecture scale with a financial summary that is shown on a web page and also printed. Split the work into steps (gather data, calculate, present), and arrange the dependencies so that the most important component (the calculation) is protected from changes in the less important ones (the presentation). Higher-level components should be protected from lower-level ones, and the arrows between components should point toward the thing you want to protect.
Chapter 9: LSP, the Liskov Substitution Principle
Anything that uses a base type should work with any subtype. The classic failure is Square extending Rectangle: setting a square’s width also changes its height, which breaks code that assumed the two are independent. The chapter lifts this to architecture: if a set of services is meant to be interchangeable (such as several taxi dispatch services behind one REST interface), then each must honour the same contract, or the calling code fills up with special cases for each service.
Chapter 10: ISP, the Interface Segregation Principle
Do not depend on things you do not use. In code this means small interfaces. At architecture level it means not depending on a framework or module that drags in extras. If your system depends on a framework that depends on a database you do not use, a change in that database can force you to rebuild and redeploy.
Chapter 11: DIP, the Dependency Inversion Principle
Code that implements high-level policy should not depend on low-level details; both should depend on abstractions. The chapter turns this into working rules:
- Prefer depending on abstract interfaces over volatile concrete classes.
- Do not derive from volatile concrete classes.
- Do not override concrete functions.
- Never mention the name of anything concrete and volatile.
Stable things such as the standard library can be used directly. Creating concrete objects is handled by factories, and a small number of concrete classes are unavoidable; they live in the main component (see chapter 26).
5. Part IV: Component principles
If the SOLID principles tell you how to arrange bricks, the component principles tell you how to arrange rooms. A component is a unit you can deploy independently: a jar, a Gradle module, a library, a service.
Chapter 12: Components
A short history: in the early days programmers had to place their programs at fixed memory addresses, and then relocatable code and linkers let them split programs into pieces. As programs grew, linking got slow, and eventually plug-in components, such as shared libraries and jars, became the norm. Today the units of deployment (a module, a package) are the basic components of an architecture.
Chapter 13: Component cohesion
Which classes belong in which component? Three principles pull against each other:
| Principle | In plain words | It is the component version of |
|---|---|---|
| REP, Reuse/Release Equivalence | The unit of reuse is the unit of release. Things you release together should be useful together, with a version number and release notes. | (the basis for everything else) |
| CCP, Common Closure | Gather classes that change for the same reason and at the same time. Separate those that change at different times. | SRP |
| CRP, Common Reuse | Do not force users to depend on things they do not need. Classes used together go together. | ISP |
Chapter 14: Component coupling
Now the relationships between components. Three principles:
- ADP, Acyclic Dependencies Principle. The component dependency graph must have no cycles. With a cycle, no component can be built, tested or released alone. Break a cycle by applying dependency inversion (introduce an interface) or by extracting a new component both sides depend on. The author also notes that the structure drifts over time, so it needs to be watched and not designed once.
- SDP, Stable Dependencies Principle. Depend in the direction of stability. A component that many others depend on is hard to change, so it should be stable, and volatile components should not be depended on.
- SAP, Stable Abstractions Principle. A stable component should be abstract, so it can be extended without being modified. A component that is both stable and concrete is a pain.
The chapter gives a simple measure of stability: I = Fan-out / (Fan-in + Fan-out). Fan-in is how many other components depend on this one; fan-out is how many it depends on. I = 0 is maximally stable (everyone depends on it, it depends on nothing); I = 1 is maximally unstable. A similar ratio, A = abstract elements / all elements, measures how abstract a component is. Plot components with I and A and the good ones sit near the line A + I = 1, called the main sequence. Components in the corner of low abstraction and high stability (the “zone of pain”) and those of high abstraction and low stability (the “zone of uselessness”) are the ones to worry about.
| Example component | Fan-in | Fan-out | I | Reading |
|---|---|---|---|---|
domain (business rules) | 4 | 0 | 0.0 | Very stable, so keep it abstract and free of details. |
web (UI) | 0 | 3 | 1.0 | Unstable, and that is fine: it is easy to change. |
database | 1 | 2 | 0.67 | Leans unstable. Nothing important should depend on it. |
6. Part V: Architecture
This is the longest part and the heart of the book. Chapters 15 to 29 cover what architecture is, how to draw boundaries, and how the pieces of a clean architecture fit.
Chapter 15: What is architecture?
The architecture of a system is the shape given to it by those who build it. That shape exists to make the system easy to develop, deploy, operate and maintain. The most important job of the architect is to keep options open: leave decisions about databases, frameworks and servers as late as possible, because the longer you wait, the more you know. A good architecture lets you build and test the business rules without choosing any of them yet. The author illustrates this with the idea that devices should be independent of the policy, so you can swap hardware or UI without touching the rules.
Chapter 16: Independence
A good architecture supports the use cases, and supports four kinds of independence:
- Operation: components can be scaled or changed in how they run.
- Development: several teams can work without stepping on each other (Conway’s law says the system will mirror the organisation anyway).
- Deployment: parts can be released without redeploying everything.
- Leaving options open: the most important of all.
The chapter also separates true duplication from accidental duplication. Two pieces of code that look alike but change for different reasons are not duplicates; merging them couples things that should be independent. Decoupling can happen at different levels (source code, deployment units or services), and you choose the level by how much independence you need.
Chapter 17: Boundaries, drawing lines
A boundary is a line between things that matter and things that do not. The business rules matter; the database and the GUI are details. Draw the line so that the details depend on the business rules, never the other way. The author tells how the wiki application FitNesse was built without a database at first, using plain files behind an interface. The database decision was deferred and, in the end, never needed. The lesson is the “plugin” idea again: the GUI and the database are plug-ins to the business rules.
Chapter 18: Boundary anatomy
What does a boundary look like in practice? Most simply, a call across a boundary in the same program (a monolith) is a function call through an interface. A boundary can also be a deployment unit, a thread, a process, or a network service. The further out you go, the more expensive crossing becomes, but the principle is the same: the dependency across the line points toward the higher-level side. Most systems have a few boundaries of the cheaper kind and fewer of the costly kind.
Chapter 19: Policy and level
A policy is a rule about how the system behaves, and its level is its distance from the inputs and outputs. The further away, the higher the level. In the book’s small encryption example, the translate step is a higher-level policy than the read and write steps. The rule: source dependencies should be decided by level, not by the direction of data flow. High-level policy should not depend on low-level details even if the data passes through them.
Chapter 20: Business rules
Entities hold critical business rules and the data they work on, such as “a loan accrues interest at this rate”, which would exist even if there were no computer. Use cases describe application-specific rules: how a user, in one scenario, asks the entities to do their work. Use cases take simple request objects and return simple response objects, which are plain data and know nothing about the web or database.
class Loan(private val principal: Money, private val annualRate: Double) { // Entity
fun interestFor(months: Int): Money = principal * (annualRate / 12 * months)
}
data class QuoteRequest(val principal: Money, val months: Int) // plain data in
data class QuoteResponse(val interest: Money) // plain data out
class QuoteLoan(private val annualRate: Double) { // Use case
fun execute(request: QuoteRequest) =
QuoteResponse(Loan(request.principal, annualRate).interestFor(request.months))
}
Chapter 21: Screaming architecture
If you look at the plans for a house, you should be able to tell if it is a house or a library. In the same way, the top-level structure of a code base should shout its purpose: “this is a health-care system”, not “this is a Spring application”. A framework is a tool, not a way of life. If the first thing you see in a project is the framework’s folder layout, the architecture is shouting about the wrong thing. A good test: can you run the use cases without the web server and database?
// Screams "framework" // Screams "library system"
com.example/ com.example.library/
controllers/ borrowing/
services/ catalogue/
repositories/ membership/
models/ fines/
Chapter 22: The Clean Architecture
This chapter combines hexagonal, onion and similar architectures into one picture of concentric circles and a single rule. Because it is the best-known idea in the book, I give it its own section below, together with the original 2012 article.
Chapter 23: Presenters and Humble Objects
The Humble Object pattern splits a hard-to-test behaviour into two parts: a humble part that is so simple it barely needs testing, and a testable part with all the logic. The classic case is a GUI. The presenter takes the data from the use case and prepares everything the screen needs (strings, formatted dates, which buttons are enabled). The view is humble: it copies the prepared values onto the screen. The same idea applies to database gateways and mappers, and to service listeners.
data class OrderViewModel(val title: String, val total: String, val canCancel: Boolean)
class OrderPresenter { // testable, no Android or web code
fun present(order: OrderResponse) = OrderViewModel(
title = "Order #${order.id}",
total = "%.2f USD".format(order.total),
canCancel = order.status == Status.PENDING,
)
}
// The view (Activity, Fragment, web page) just copies viewModel fields onto the screen.
Chapter 24: Partial boundaries
Full boundaries are expensive: reciprocal interfaces, input and output data structures, and separate components. Sometimes you want to prepare for one without paying for it yet. The author gives three options: build everything but keep it in one component (skip the last step); use a one-dimensional boundary, an interface that only one side honours (a simple Strategy); or a facade, which is the cheapest and leaves the dependency pointing the wrong way. These are bets that you will not need the full thing, and they can decay.
Chapter 25: Layers and boundaries
Using a small text game (“Hunt the Wumpus”) as an example, the chapter shows that boundaries show up more often than you expect: between the user interface and the game rules, between the rules and the data storage, even between the rules and the choice of language (English versus another). The question is always whether to build a boundary now (cost) or to risk retrofitting later (also cost). The author’s answer is to watch for the signs, and not to over-engineer or ignore the issue: you need to judge.
Chapter 26: The Main component
Main is the dirtiest component, and that is by design. It is the entry point; it creates all the factories, strategies and global facilities, and wires them into the high-level policy. It is a plug-in to the application. You can have several (a development one, a test one, a production one), each with a different configuration.
fun main() {
val books = InMemoryBooks() // concrete detail, chosen here only
val presenter = ConsolePresenter()
val useCase = BorrowBook(books, presenter)
val controller = BorrowController(useCase)
controller.onBorrowClicked(bookId = "b1", memberId = "m7")
}
Chapter 27: Services, great and small
Service-oriented architecture and microservices are popular, but the chapter says a service is just a way of drawing a boundary, not an architecture in itself. It argues against two common beliefs. Decoupling fallacy: services that share data formats or meanings are still coupled; changing a shared field forces changes everywhere. Independent development and deployment fallacy: a feature that cuts across many services (the book’s example adds “delivery of kittens” to a taxi app) needs all the services to change together. The answer is that services should be built with internal component-based architecture, and cross-cutting concerns should be handled through well-designed boundaries within and between services.
Chapter 28: The test boundary
Tests are part of the system architecture. They are the outermost circle, depend on everything, and nothing depends on them. The danger is the fragile test problem: if tests are tightly coupled to the structure of the system, a small change in production code breaks hundreds of tests, and developers start to avoid changing the code. The remedy is to design for testability and to give tests a testing API, a set of operations that bypass the security and expensive resources that tests do not need to repeat, and that is kept separate from the structure of the application.
Chapter 29: Clean embedded architecture
Embedded software has the same problems with a different flavour. The author quotes a rule often attributed to Kent Beck: make it work, make it right, make it fast, and says embedded developers tend to stop after step one. Software that is welded to hardware turns into firmware: code that cannot be tested off the device and that dies with the chip. The remedy is to separate layers. The hardware sits at the bottom, a hardware abstraction layer (HAL) hides it, and the application logic sits above and knows nothing about the device. Even the operating system should be hidden behind an interface. The test the book proposes: when the hardware changes, how much of the code has to change?
7. The 2012 article: The Clean Architecture
Long before the book, Robert Martin published a blog post on 13 August 2012, The Clean Architecture. It is short, free, and worth reading in full. The chapter 22 of the book is built on it.
The architectures it builds on
The article starts by naming several earlier ideas that have the same aim: Hexagonal Architecture (ports and adapters, by Alistair Cockburn), Onion Architecture (by Jeffrey Palermo), Screaming Architecture (from the author’s own earlier post), DCI (by James Coplien and Trygve Reenskaug) and BCE (by Ivar Jacobson). All of them separate software into layers, with at least one layer for business rules and another for interfaces. Systems built this way tend to be:
- Independent of frameworks. You use the framework as a tool, not as a mould.
- Testable. Business rules can be tested without the UI, database or web server.
- Independent of the UI. The UI can change from web to console without changing the rules.
- Independent of the database. You can swap one database for another.
- Independent of any external agency. The business rules know nothing about the outside world.
The circles and the Dependency Rule
- Entities (innermost): enterprise-wide business rules, as objects or as data plus functions. They are the least likely to change when something external changes.
- Use Cases: application-specific business rules. They orchestrate the flow of data to and from the entities and direct them to apply their rules. A change here should not touch the entities, and a change in the database or UI should not touch the use cases.
- Interface Adapters: converters between the format that suits the use cases and entities and the format that suits the outside agency. The whole MVC set (presenters, views and controllers) lives here, and so does all the SQL.
- Frameworks and Drivers (outermost): the database, the web framework and other tools. This is glue code, and it is where the details go.
The Dependency Rule: source code dependencies can only point inwards. Nothing in an inner circle can know anything about an outer one: not a function, class, variable or data format.
Crossing a boundary
There is an apparent conflict: when a user clicks a button, control goes from the controller to the use case and then to the presenter. But the use case must not depend on the presenter, since that would point outward. The answer is dependency inversion. The use case calls an interface (the output port) that is defined in the use case circle, and the presenter in the outer circle implements it. Source dependencies and the flow of control point in different directions at that point.
Data across boundaries
What passes across a boundary should be simple, isolated data: basic structures, data transfer objects or plain arguments. Do not pass entities or database rows outward or inward. Data should cross in the form that is most convenient for the inner circle. The article ends with the claim that following these rules is easy and gives a system that is testable by nature, where the database or web framework can be replaced with little work.
8. Part VI: Details
This part makes one point five times: the things we treat as the centre of the system (database, web, frameworks) are details at the edge.
Chapter 30: The database is a detail
The data model is important; the database is a mechanism. A relational database is a good way to store and query data, but it is not the architecture. When disks were slow and spinning, database design mattered a lot, but with fast memory and flash the performance reasons for letting it shape your code have faded. Keep the business rules ignorant of the storage, and access data through an interface so you can change it later.
interface BookRepository { // owned by the business rules
fun find(id: String): Book?
fun save(book: Book)
}
// SQL, Room, files or memory are all just implementations of it.
Chapter 31: The web is a detail
The author recalls how many times the industry swung between thin and fat clients, and concludes that the web is just another input and output device. Your system should not care whether it is reached through a browser, an app or a command line. The GUI is a detail, and the interface between it and the business rules should be a boundary.
Chapter 32: Frameworks are details
A framework author gives you the code and asks for your commitment in return, which the book calls an asymmetric marriage: you make a big commitment and they make none. So hold frameworks at arm’s length. Do not let your business objects inherit from framework base classes, and do not scatter framework annotations through the core. Wrap and map them at the edge.
// Bad: the core is married to the persistence framework
@Entity data class Book(@PrimaryKey val id: String, val title: String)
// Clean: a plain core, with a mapping at the edge
data class Book(val id: String, val title: String)
@Entity data class BookRow(@PrimaryKey val id: String, val title: String)
fun BookRow.toDomain() = Book(id, title)
fun Book.toRow() = BookRow(id, title)
Chapter 33: Case study, video sales
The author works through the architecture of a website that sells videos to individuals and to businesses. Start from the actors (viewer, purchaser, administrator, author) and their use cases, then draw the components: for each use case, a controller, an interactor, a presenter and a view, with entities and a gateway to the database in the middle. Then group them into components along the lines of the boundaries. The chapter's real point is the process: find actors, then use cases, then components, and arrange the arrows so that they all point toward the business rules.
Chapter 34: The missing chapter
Written by guest author Simon Brown, this chapter notes that good design can still be undone by how you implement it. It compares four ways to organise code:
| Way | Idea | Weakness |
|---|---|---|
| Package by layer | web, service, data folders | Does not show what the system does, and everything tends to become public. |
| Package by feature | one folder per feature | Better at showing purpose, but boundaries inside can blur. |
| Ports and adapters | domain inside, adapters outside | Needs discipline to keep the rule. |
| Package by component | a component bundles its logic and data access behind one entry point | Relies on the language to enforce access rules. |
The warning is about implementation details: if everything is public, then the compiler cannot enforce the architecture and a shortcut is one import away. In Kotlin, the internal modifier (visible only in the same module) and separate Gradle modules let the build do this enforcement for you.
9. Appendix: Architecture archaeology
The appendix is the author’s personal history. He walks through projects from his own career, starting in the early 1970s with hardware that had tiny memories, and moving through the decades. The technologies change completely (from machine code and punched tape to databases and the web), but the architectural problems repeat: how to be independent of the device, how to keep policy apart from mechanism, how to test, and how to survive change. The lesson of the appendix is that these principles are old and have held up, which is a good argument for learning them rather than the framework of the month.
I am deliberately general here. The appendix is a story, and the best way to take it in is to read it in the author’s own words.
10. Putting it together: a small example in Kotlin
Here is a complete, tiny system that follows the Dependency Rule: borrowing a library book. Read it from the inside out.
// ── 1. Entity (innermost): no imports from anywhere else ─────────────────
class Book(val id: String, val title: String, var borrowedBy: String? = null) {
fun borrow(memberId: String) {
check(borrowedBy == null) { "Already borrowed" }
borrowedBy = memberId
}
}
// ── 2. Use case: owns the interfaces ("ports") it needs ──────────────────
interface BookRepository { fun find(id: String): Book?; fun save(book: Book) }
interface BorrowOutput { fun borrowed(title: String); fun rejected(reason: String) }
data class BorrowRequest(val bookId: String, val memberId: String) // plain data
class BorrowBook(
private val books: BookRepository,
private val output: BorrowOutput,
) {
fun execute(request: BorrowRequest) {
val book = books.find(request.bookId) ?: return output.rejected("No such book")
try {
book.borrow(request.memberId)
books.save(book)
output.borrowed(book.title)
} catch (e: IllegalStateException) {
output.rejected(e.message ?: "Cannot borrow")
}
}
}
// ── 3. Interface adapters: implement the ports, convert formats ──────────
class InMemoryBooks(vararg initial: Book) : BookRepository {
private val store = initial.associateBy { it.id }.toMutableMap()
override fun find(id: String) = store[id]
override fun save(book: Book) { store[book.id] = book }
}
class ConsolePresenter : BorrowOutput { // a humble view of the result
override fun borrowed(title: String) = println("You borrowed \"$title\"")
override fun rejected(reason: String) = println("Sorry: $reason")
}
class BorrowController(private val useCase: BorrowBook) {
fun onBorrowClicked(bookId: String, memberId: String) =
useCase.execute(BorrowRequest(bookId, memberId))
}
// ── 4. Main: the dirtiest part, wires everything ─────────────────────────
fun main() {
val books = InMemoryBooks(Book("b1", "Clean Architecture"))
val controller = BorrowController(BorrowBook(books, ConsolePresenter()))
controller.onBorrowClicked("b1", "m7") // You borrowed "Clean Architecture"
controller.onBorrowClicked("b1", "m8") // Sorry: Already borrowed
}
| Part | Circle | Knows about |
|---|---|---|
Book | Entities | Nothing else. |
BorrowBook, ports, BorrowRequest | Use cases | Entities only. |
InMemoryBooks, ConsolePresenter, BorrowController | Interface adapters | Use cases and entities. |
main | Frameworks and drivers | Everything, as the one place that is allowed to. |
Swap the console for an Android screen, or the in-memory map for a database, and nothing from the entity or the use case changes. To test BorrowBook, pass it a fake repository and a recording output; no screen and no database are needed.
11. Where I use judgement
- Boundaries cost something. Interfaces, mappers and separate modules add code. For a small app or a prototype, a plain structure is the right call. The book itself says to watch for the need and not to build every boundary up front.
- It is a style, not the only one. The ideas overlap with hexagonal and onion architecture and with many team conventions. Use the Dependency Rule as the core idea and be flexible about the rest.
- Mapping fatigue. Converting between entity, row and view model for every feature can feel heavy. Keep separate models where the shapes really differ, and share them where they do not.
- Platform differences. Mobile apps, serverless functions and small services each have their own conventions. Kotlin helps with data classes, sealed types,
internaland Gradle modules. - Examples are dated. The book is from 2017 and uses Java and C++ examples. The ideas aged better than the code samples.
- Read the book for the stories. The anecdotes (FitNesse, the early years, the taxi-and-kittens example) carry much of the persuasion and do not fit in a summary.
12. Takeaways, checklist and references
Questions I ask when reviewing a design:
- If I delete the database or the framework, how much of the business logic has to change?
- Do all source-code arrows point toward the business rules?
- Can I run the use cases in a test with no UI and no real storage?
- Does the top level of the project show what it does, not which framework it uses?
- Is there a dependency cycle between modules?
- Is the thing everyone depends on both stable and abstract?
- Which decisions (database, web, framework) have I made earlier than I had to?
- Is each boundary earning its cost?
References
- Robert C. Martin, Clean Architecture: A Craftsman’s Guide to Software Structure and Design, Prentice Hall, 2017. Amazon · InformIT
- Robert C. Martin, The Clean Architecture, 13 August 2012.
- My other posts: Clean Code, Distilled, Designing for Change: The SOLID Principles and Write It Before You Build It.
| Term | In plain words |
|---|---|
| Architecture | The shape of a system that makes it cheap to build, change and run. |
| Dependency Rule | Source code may only depend on things closer to the business rules. |
| Entity | An object holding core business rules that would exist without the application. |
| Use case | An application-specific rule describing one thing a user can do. |
| Boundary | A line between two parts, crossed through an interface. |
| Port / Adapter | An interface owned by the core, and the outer code that implements it. |
| Humble Object | A hard-to-test part made so simple that it barely needs tests. |
| Component | An independently deployable unit, such as a Gradle module or a jar. |
| Stability (I) | How hard a component is to change, measured by what depends on it. |
| Screaming architecture | A structure whose top level reveals what the system is for. |
| Main component | The entry point that creates and connects everything. |
| Firmware | Software so tied to hardware that it cannot outlive it. |