This is Alistair Cockburn's canonical Ports & Adapters example. The odd-looking interface names — ForCalculatingTaxes, ForGettingTaxRates — are deliberate: each interface is a port, named after the conversation it exists for, not after the class that implements it.
The Java snippet packs the whole pattern into five declarations. Sorting them by role makes the shape obvious:
The driving port — the use case the application offers to the outside world. Anyone who wants taxes calculated talks through this.
The driven port — what the core needs from the outside world. It says "give me a rate" without caring where rates come from.
The application core. It implements the driving port and depends only on the driven port's abstraction, injected via constructor. Zero knowledge of concrete adapters.
A driven adapter: one concrete way to satisfy ForGettingTaxRates. Here it's a hardcoded 15%, but it could be a database, an API, or a test stub — the core can't tell the difference.
Main plays the last role: the composition root. It's the only place that knows concrete types, wiring FixedTaxRateRepository into TaxCalculator. That's the Dependency Inversion Principle in one sentence — high-level policy and low-level detail both depend on an abstraction, and the arrows of source-code dependency point inward, against the flow of control.
A line-for-line port would work, but Kotlin lets the pattern breathe: single-method interfaces become fun interface, constructor injection collapses into the primary constructor, and single-expression functions drop the ceremony.
// Ports — named for the conversation, not the implementation fun interface ForCalculatingTaxes {1 fun taxOn(amount: Double): Double } fun interface ForGettingTaxRates { fun taxRate(amount: Double): Double } // Core — depends only on the driven port class TaxCalculator( private val taxRateRepository: ForGettingTaxRates2 ) : ForCalculatingTaxes { override fun taxOn(amount: Double) = amount * taxRateRepository.taxRate(amount)3 } // Driven adapter — one concrete answer to "what's the rate?" class FixedTaxRateRepository : ForGettingTaxRates { override fun taxRate(amount: Double) = 0.15 } // Composition root fun main() { val calculator: ForCalculatingTaxes = TaxCalculator(FixedTaxRateRepository()) println(calculator.taxOn(100.0)) // 15.0 // fun interface bonus: SAM conversion — an adapter in one lambda val testCalculator = TaxCalculator { 0.10 }4 println(testCalculator.taxOn(100.0)) // 10.0 }
fun interface marks a single-abstract-method interface, enabling SAM conversion. The port stays a real named abstraction — you just get lambda ergonomics for free.
Primary constructor + private val replaces the field-declaration / constructor / assignment triplet from Java. Injection and storage in one line, and the dependency is immutable.
Expression body (= instead of { return … }) fits one-liner domain logic. The type is still checked against the port's signature.
SAM conversion: TaxCalculator { 0.10 } creates an anonymous ForGettingTaxRates from a lambda — an entire test double with no MockK, no class declaration.
The whole payoff of the pattern is this move: TaxCalculator never changes, only what's plugged into ForGettingTaxRates. Pick an adapter, then run taxOn().
Notice the core's line never varies: amount * taxRateRepository.taxRate(amount). All the behavioral change lives on the far side of the port.
Forty lines hide the shape that matters once the app grows. Blow the hexagon up and two independent structures appear at once: a left/right division by who starts the call, and inner/outer layers by how much each ring is allowed to know. They are not the same axis — mistaking one for the other is the usual source of confusion.
The two sides // split by direction of control
A port is either something the app offers or something it requires. That one question sorts every adapter you will ever write onto the left or the right of the hexagon.
Start the conversation: a REST controller, a CLI command, a scheduled job, a test. Each depends on a driving port such as ForCalculatingTaxes and translates the outside world into a call on the core.
Get called by the app: a repository, a payment gateway, an email sender. The core owns the driven port ForGettingTaxRates; the adapter merely implements it. That ownership is what inverts the dependency.
The three layers // split by what each ring may import
Cut the same hexagon the other way — from the core outward — and you get three concentric rings. An outer ring may know the ring inside it; never the reverse.
Pure Kotlin — value objects, entities, and invariants that hold no matter how the app is delivered or stored. Imports nothing but the standard library. In the toy it is a bare Double; in real code a Money / TaxYear model.
Where TaxCalculator and the ports live. Depends on the domain and on port abstractions only — never a concrete adapter. This ring is the inside of the hexagon: the part you are protecting.
Everything that touches the world — Spring, JDBC, HTTP clients, the CLI, test doubles. Each adapter implements a driven port or calls a driving one. Endlessly swappable, precisely because nothing inside imports it.
Source-code dependencies point inward only: adapters know the application, the application knows the domain, the domain knows nothing but itself. Control flows both ways across a port — a driving adapter calls in, the app calls out — but the import arrow always points at the more stable, more abstract ring. On the driven side those two directions oppose: the app declares ForGettingTaxRates, and a low-level adapter implements it. That inversion is the entire point of the pattern.
How it lands in folders // a single-module Kotlin layout
Package structure makes the two axes literal. The top split is by layer — domain / application / adapter; inside, in and out split by side. Colour carries the story: purple is the driving side, orange the driven side, blue the protected core.
com.acme.tax │ ├─ domain/ // innermost — pure model, no framework imports │ └─ Money.kt // value objects, entities, invariants │ ├─ application/ // the core: use cases + the ports they speak through │ ├─ port/ │ │ ├─ in/ │ │ │ └─ ForCalculatingTaxes.kt // driving port — what the app offers │ │ └─ out/ │ │ └─ ForGettingTaxRates.kt // driven port — what the app needs │ └─ service/ │ └─ TaxCalculator.kt // use case — implements in, depends on out │ ├─ adapter/ │ ├─ in/ // PRIMARY / driving — initiate calls into the app │ │ ├─ web/TaxController.kt // REST → ForCalculatingTaxes │ │ └─ cli/TaxCli.kt │ └─ out/ // SECONDARY / driven — the app calls these │ └─ persistence/ │ └─ FixedTaxRateRepository.kt // implements ForGettingTaxRates │ └─ TaxApplication.kt // composition root — the only file that knows concretes
port/in vs port/out — same word, opposite meaning. in holds driving ports the app offers; out holds driven ports the app requires. adapter/in and adapter/out mirror them one ring further out.
Adapters only translate. TaxController turns an HTTP request into calculator.taxOn(…); FixedTaxRateRepository turns "what's the rate?" into a value. No domain logic ever leaks into the edge ring.
TaxApplication.kt is the seam. The one yellow file that names concrete types, wiring FixedTaxRateRepository into TaxCalculator. Swap in a real database here and not one blue or purple file changes.
Want the compiler — not discipline — to enforce the arrows? Promote each layer to its own Gradle module: :domain, :application, :adapter-web, :adapter-persistence, :bootstrap, and let only :bootstrap depend on the adapters. Now an import that points the wrong way simply won't compile.
In a toy example the ports look like overhead. The value shows up when the "rate repository" is a PMS integration, a payment provider, or anything else you don't control:
The lambda stub above is the test setup. The core is exercised with zero I/O, zero mock framework config — connascence of name on the port signature is the only coupling left.
The adapter imports the port; the core imports nothing concrete. Swapping vendors or protocols becomes a new adapter file, not a change to domain logic.
ForGettingTaxRates reads as a requirement of the core. Compare with TaxRateRepositoryInterface, which just names an implementation twice.
Only main() (or your DI container / Spring config) knows concrete classes. Configuration decisions live in exactly one place.