Bend, or Break

5 min read

Core idea

Code must bend to survive change

Software lives in a world where requirements shift, services come and go, and yesterday's assumptions become tomorrow's bugs. Code that is rigidly wired together cannot absorb that turbulence. It snaps. Code that is loose, modular, and configurable can absorb it — and the whole topic is a tour of the structural choices that decide which kind of code you have.

Coupling is the master enemy

Thomas and Hunt frame every topic in this topic as a different attack on a single villain: coupling. When two pieces of code know too much about each other — a method call chain that traverses five objects, a class hierarchy five layers deep, a constant baked into a deployment, a function that mutates a shared list — change in one place ripples through all the others. The Pragmatic prescription is not "no coupling" (impossible) but the minimum coupling consistent with the problem, applied at every scale: between statements, between objects, between modules, between system and configuration.

Why it matters

Coupling is transitive — and it compounds

If A depends on B, and B depends on M and N, then A actually depends on B, M, and N. You did not write that dependency; it was forced on you by transitivity. When a system grows, transitive coupling silently turns "fix one bug" into "redeploy five services and pray." The cost of coupling is not visible at the moment you create it — it shows up months later when a small change requires a team meeting.

Flexibility is the substrate of every other Pragmatic value

The earlier topics argued for reversible decisions, tracer bullets, and prototypes. None of those techniques work in a codebase whose shape is frozen. Bend-or-break is the structural layer that makes everything else — refactoring, iteration, agility — actually possible. Lose flexibility and you lose the right to change your mind.

Key takeaways

The topics in this topic

Decoupling

Tip: Decoupled code is easier to change.

Coupling shows up in three classic patterns the topic warns about. Train wreckscustomer.orders().find(id).totals().applyDiscount(d) — drag your method across five layers of abstraction and tie it to every one of them. The Law of Demeter ("only talk to your immediate friends") suggests rewriting the call so the customer object exposes the operation you want and hides how it is composed. Global data is the second pattern: a singleton or static accessor that anyone can read or mutate creates invisible coupling between every caller. Treat globals as shared mutable resources — including the loggers and config objects you may not have thought of that way. Inheritance is the third, deferred to its own topic below. The unifying instinct is to "keep your code shy" — only deal with what you directly know about, and refuse to reach across other people's boundaries.

A concrete example. A payment service that calls order.getCustomer().getAddress().getCountry().getTaxRate() is coupled to four classes' internal structure. Replace it with order.taxRateForBilling() and only Order knows the chain. When the address model changes — say, country becomes a region code — one class changes, not every caller.

Juggling the Real World

Tip: Listen to your inner lizard. The real world is asynchronous.

The real world is full of events: button clicks, timeouts, file lines matching a pattern, user logins. Procedural code that pretends the world is linear ends up with sprawling conditionals and brittle state checks. The topic offers four strategies for handling events without that mess. Finite State Machines make legal transitions explicit — a download is either idle, fetching, paused, complete, or failed, and the diagram of legal moves is the code. The Observer pattern lets one object publish changes to a list of registered listeners. Publish/Subscribe generalises observers across processes via a broker, so the publisher and subscribers never need to know each other. Reactive streams treat events as a flow you can filter, throttle, and combine like collections.

Concretely: instead of a UI thread that polls "has the network finished?" every 100ms, the network layer publishes a download.complete event and the UI subscribes. The polling loop, the timeout state, and the temporal coupling all vanish together.

Transforming Programming

Tip: Programming is about code, but programs are about data.

Most code is a sequence of transformations: input goes in, transformed output comes out. Object-oriented code hides that pipeline by smearing state across mutable objects. Functional pipelines make it visible. text | parseLines | filterErrors | groupBy(file) | report reads as a recipe — each step is a small pure function whose only contract is "data in, data out." Adding a stage, swapping a stage, or running the pipeline against a different input is a one-line change. The data shape becomes the API.

Many imperative languages now support this style with method chaining, generators, or async iterators. Even when the language does not give you pipe operators, the discipline of "think in transformations" pays off: it pushes mutability and side effects out to the edges where they can be controlled.

Inheritance Tax

Tip: Don't pay inheritance tax. Inheritance is rarely the answer.

Subclassing was invented for two purposes — sharing code and modelling type relationships. Both uses charge a heavy tax. Sharing code through inheritance couples the child to the parent's full surface area, the parent's parent, and so on; the code that uses the child is coupled to all of them too. Modelling types ("a Car is-a Vehicle") freezes a taxonomy at design time and forces every later refinement into the same hierarchy. The topic recommends three alternatives, all looser. Interfaces and protocols describe what an object can do without dictating where the implementation comes from. Delegation asks an object you hold for the behavior, so you can swap the delegate. Mixins, traits, and categories add capabilities to many classes without coupling them through a common ancestor.

A practical refactor: instead of class AdminUser extends User, make User hold a permissions strategy object. New roles become new strategies, not new subclasses. Permissions can change at runtime. The hierarchy stops growing.

Configuration

Tip: Parameterize your app using external configuration.

Anything that might change after deployment — feature flags, URLs, credentials, log levels, tuning constants, locales, business rules — should live outside the source code. Hard-coding a value freezes a decision into a build artifact; externalising it makes the decision reversible without code changes. The topic recommends storing configuration in a place that respects the system's deployment model (environment variables for cloud services, files for self-hosted tools, a config service for fleets) and treating the configuration boundary as a stable, versioned API of its own.

The deeper move is conceptual: treat the application as a service that consumes configuration, not a binary that contains it. A feature flag is not just a boolean; it is the difference between "we ship code that we cannot turn off" and "we ship code and decide later whether to enable it."

Mental model

Mental model

Continue exploring

Tags