Clean Code Chapter 8: Boundaries
Principles
Third-party libraries, vendor APIs, and legacy modules are not under your control. Clean systems meet them at a deliberate boundary so the rest of the code depends on interfaces you own.
Wrap what you do not control
Adapters translate foreign types into your vocabulary. The rest of the app talks to Sensors, not Map<String, Sensor> sprinkled everywhere. When the vendor changes, you change one place.
Do not pass boundary types through the system
Maps, vendor DTOs, and SDK clients are fine at the edge. Dragging them into domain logic couples your core to someone else’s release notes.
Learning tests
Write small tests that encode how a library actually behaves. They teach the API, document assumptions, and fail loudly when an upgrade changes semantics, cheaper than rediscovering behavior in production.
Code that does not exist yet
When a collaborator is not ready, program against an interface you own. That keeps your module moving and defines the boundary before the third-party (or teammate) API hardens in the wrong shape.
Clean boundaries are tested boundaries
Outbound tests should exercise the interface the same way production code does. Good boundaries accommodate change without huge rework.
flowchart LR
App[Your domain] --> Adapter[Your adapter]
Adapter --> Lib[Third-party API]
Learn[Learning tests] -.-> Lib
Out[Boundary tests] -.-> Adapter
In code
Examples below are in Java.
Leaking a boundary type:
Map<String, Sensor> sensors = new HashMap<>();
Sensor sensor = sensors.get(sensorId);
Encapsulate the boundary:
public class Sensors {
private final Map<String, Sensor> sensors = new HashMap<>();
public Sensor getById(String id) {
return sensors.get(id);
}
public void put(Sensor sensor) {
sensors.put(sensor.id(), sensor);
}
}
Learning-test sketch:
@Test
void log4jWritesToMemoryAppender() {
Logger logger = Logger.getLogger("test");
MemoryAppender appender = new MemoryAppender();
logger.addAppender(appender);
logger.info("hello");
assertThat(appender.messages()).contains("hello");
}
Takeaway
Wrap third-party code behind your own types. Keep vendor details at the edge. Learning tests make upgrades less painful.