This chapter is about the “edge” of your system—the place where your clean, pristine code meets the “messy” outside world (third-party libraries, APIs, or open-source frameworks).
Uncle Bob argues that we often give up control of our systems by letting foreign code leak into our business logic.
1. Using Third-Party Code
Providers of libraries want their code to be broadly applicable, so they design “wide” interfaces. For example, a java.util.Map has dozens of methods like clear(), addAll(), and remove().
If you pass a Map around your system, any part of your code can delete your data.
The “Clean” solution: Encapsulation Instead of passing the Map everywhere, wrap it in a class that only exposes what you need.
// WRAPPING A BOUNDARY
class Sensors {
private val sensors = mutableMapOf<String, Sensor>()
fun getById(id: String): Sensor? {
return sensors[id]
}
// Notice we DON'T expose clear(), remove(), etc.
}
2. Exploring and Learning Boundaries
When using a new library, don’t just start writing your production code. Write Learning Tests.
-
You write small tests to verify you understand how the library works.
-
When the library updates, you run your learning tests to see if anything broke.
3. Using Code That Does Not Yet Exist
Sometimes you need to write code that depends on a module being developed by another team. Instead of waiting or writing “fake” messy code, you should define the interface you wish you had.
This creates a clear boundary: your code talks to an interface, and when the other team finishes, you just write an Adapter to bridge the gap.
Kotlin Corner: The “Map” Temptation
In Kotlin, we often use Map and List quite freely because of their excellent extension functions. However, if your Order class is just a Map<String, Any>, you’ve lost all type safety and created a boundary mess.
Interactive Checkpoint:
Imagine you are using a 3rd-party Payment API called SuperPay. It requires a Map<String, String> for configuration and throws 5 different types of custom exceptions.
If you follow Chapter 8, would you let your OrderProcessor call SuperPay directly, or would you create a “Gateway” or “Wrapper” class? Why?
Solution:
That is the Adapter Pattern in action. By creating a Gateway, you own the interface, and SuperPay becomes a detail hidden behind a wall.
Uncle Bob points out that this also makes testing significantly easier. You don’t want your unit tests trying to hit a real Payment API; you just want to mock your Gateway.
1. The “Architectural” Way (Focus on Stability)
“I would implement a Gateway/Wrapper to protect my business logic from external volatility. By creating a stable interface that we own, we ensure that changes in the third-party implementation are confined to a single Adapter, preventing ‘vendor leak’ from polluting our core domain.”
2. The “SOLID” Way (Focus on Principles)
“Using a Wrapper allows us to follow the Dependency Inversion Principle. Our high-level business rules depend on an abstraction (our interface) rather than a low-level detail (the third-party API). This makes the system more maintainable and significantly easier to test using mocks.”
3. The “Concise” Way (Perfect for Interviews)
“I’d wrap the third-party API to create a Boundary. This decouples our system from external dependencies, ensuring that an API change only requires a single point of repair rather than a shotgun-surgery refactor across the entire codebase.”
Why these phrases matter (Chapter 8 Context):
-
“Vendor Leak”: When you see
SuperPayExceptionorSuperPayMapinside yourOrderService, the vendor has “leaked” into your code. -
“Shotgun Surgery”: This is the “Smell” (Chapter 17) where one small change in an external API forces you to make tiny changes in dozens of different classes.
-
“Stable Abstraction”: Your interface shouldn’t change even if the provider does. If SuperPay uses a
Mapbut the new provider usesJSON, your interface stays the same:process(payment).