Book summary · Software architecture · Kotlin

Clean Architecture, Chapter by Chapter: A Kotlin Companion to Uncle Bob’s Guide

Khairul Alam·October 2026·~45 min read
Clean ArchitectureBook summaryKotlinSoftware design

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.

The whole post in 60 seconds
  • 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
  1. About the book and where to get it
  2. Part I: Introduction (Chapters 1 and 2)
  3. Part II: Programming paradigms (Chapters 3 to 6)
  4. Part III: Design principles (Chapters 7 to 11)
  5. Part IV: Component principles (Chapters 12 to 14)
  6. Part V: Architecture (Chapters 15 to 29)
  7. The 2012 article: The Clean Architecture
  8. Part VI: Details (Chapters 30 to 34)
  9. Appendix: Architecture archaeology
  10. Putting it together: a small example in Kotlin
  11. Where I use judgement
  12. 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.

DetailValue
TitleClean Architecture: A Craftsman’s Guide to Software Structure and Design
AuthorRobert C. Martin
ISBN-10 / ISBN-130134494164 / 978-0134494166
Buy the bookAmazon · InformIT (publisher’s store)
Free articleThe 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.

Important, not urgentArchitecture, structureEasy to postpone Important and urgentReal emergencies Not important, not urgent Urgent, not importantMany feature requestsFeel pressing, change little ← less urgent      more urgent →
Architecture lives in the top-left box, which is why it is so easily crowded out.

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:

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:

PrincipleIn plain wordsIt is the component version of
REP, Reuse/Release EquivalenceThe 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 ClosureGather classes that change for the same reason and at the same time. Separate those that change at different times.SRP
CRP, Common ReuseDo not force users to depend on things they do not need. Classes used together go together.ISP
REP: reusable CCP: easy to change CRP: no extras You cannot maximise all three. Early on favour CCP; as others reuse your code, lean toward REP and CRP.
The tension triangle. Where you stand on it shifts as a project matures.

Chapter 14: Component coupling

Now the relationships between components. Three principles:

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 componentFan-inFan-outIReading
domain (business rules)400.0Very stable, so keep it abstract and free of details.
web (UI)031.0Unstable, and that is fine: it is easy to change.
database120.67Leans 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:

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:

The circles and the Dependency Rule

Entities Use Cases Interface Adapters Frameworks and Drivers Outside: details(web, database, UI) Inside: policy(business rules) Source code dependencies point inward only
The four circles of the article. The names and exact number of circles are schematic; the rule is what matters.
  1. Entities (innermost): enterprise-wide business rules, as objects or as data plus functions. They are the least likely to change when something external changes.
  2. 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.
  3. 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.
  4. 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.

Controller Use Case «Output Port»interface Presenter control flow → the presenter implements the port (dashed) The use case never mentions the presenter, so dependencies still point inward.
Dependency inversion at a boundary: control goes out, source code points in.

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:

WayIdeaWeakness
Package by layerweb, service, data foldersDoes not show what the system does, and everything tends to become public.
Package by featureone folder per featureBetter at showing purpose, but boundaries inside can blur.
Ports and adaptersdomain inside, adapters outsideNeeds discipline to keep the rule.
Package by componenta component bundles its logic and data access behind one entry pointRelies 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
}
PartCircleKnows about
BookEntitiesNothing else.
BorrowBook, ports, BorrowRequestUse casesEntities only.
InMemoryBooks, ConsolePresenter, BorrowControllerInterface adaptersUse cases and entities.
mainFrameworks and driversEverything, 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

12. Takeaways, checklist and references

Questions I ask when reviewing a design:

References

TermIn plain words
ArchitectureThe shape of a system that makes it cheap to build, change and run.
Dependency RuleSource code may only depend on things closer to the business rules.
EntityAn object holding core business rules that would exist without the application.
Use caseAn application-specific rule describing one thing a user can do.
BoundaryA line between two parts, crossed through an interface.
Port / AdapterAn interface owned by the core, and the outer code that implements it.
Humble ObjectA hard-to-test part made so simple that it barely needs tests.
ComponentAn 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 architectureA structure whose top level reveals what the system is for.
Main componentThe entry point that creates and connects everything.
FirmwareSoftware so tied to hardware that it cannot outlive it.
← Back to all writing