Breaking Things Apart So They Actually Come Back Together

Composition is one of those terms people throw around until it means nothing, but it describes something very specific. When you compose things, you take independent parts and combine them so the result does more than any single part could. In technical work this looks like building functions that return functions, combining small parsers into big ones, or stitching together HTTP middlewares. It also applies to writing. You combine sentences into paragraphs, paragraphs into sections, sections into arguments. The skill is knowing what to make independent in the first place. I have spent years watching teams either love compositional design or accidentally fall into it without understanding what they are doing. The happy cases are obvious. The unhappy ones are worse because nobody notices anything is wrong until a refactor breaks three unrelated features. Composition sounds simple because it is simple in theory. It is much less forgiving in practice, especially when the boundaries between components are blurry. The first thing to understand is that composition is not the same as coupling. Coupling means two things depend on each other directly. Composition means each thing can stand alone and you connect them at the edges. A poorly composed system still ends up with hidden dependencies anyway because the original designer did not define clear input and output contracts for each piece.

When I started working with compositional APIs, I assumed the hard part was the pattern itself. I was wrong. The hard part is naming the pieces well enough that the next person who touches the code does not have to reverse engineer the boundary decisions. I once spent two days tracing a bug that came down to a parser combinator quietly consuming whitespace inside a token it should not have touched. The tokenizer and the parser looked correct in isolation. Together they formed a subtle grammar that silently accepted malformed input. The fix was narrow: add an explicit span check between composition steps and reject anything that crosses an unexpected boundary.

How Composition Actually Works

At the lowest level, composition requires three things. You need components that are self-contained. You need a composition operator that combines them predictably. You need a way to inspect the combined result without losing visibility into which part produced what. In programming, the composition operator is often just function application. You pass the output of one function into the input of another. Sometimes it is composition of types, like using generics to parameterize behavior. Sometimes it is data pipelines, where each stage transforms a record in place. The pattern repeats whether you are working in Rust, JavaScript, Python, or a DSL. The language matters less than the contract between stages. In writing, composition works the same way structurally. You have a claim, a piece of evidence, and an explanation. Each unit does one job. You arrange them so the reader can trace your logic without filling in gaps. The failure mode is the same as in code: if a component assumes the reader already knows something it was supposed to explain, the whole chain weakens.

Get the Full Details

The Language of Composition Third Edition Reading, Writing, Rhetoric by Renee H. Shea - EBooks-Store
The Language of Composition Third Edition Reading, Writing, Rhetoric by Renee H. Shea - EBooks-Store

Compositional Thinking In Practice

Start by listing the behaviors your system must support, then group related behaviors into components. Test each component against edge cases before you compose anything. If a component cannot handle a reasonable boundary condition on its own, composition will not save it later. I usually write a small driver script that runs every component in isolation with synthetic inputs before I combine them. This catches silent type mismatches, off-by-one errors, and missing error paths early. When you compose, keep the interface between components minimal and explicit. A single function signature tells you everything you need to know about what a component accepts and returns. If you find yourself adding extra parameters just to share state between stages, that is a sign the boundary is in the wrong place. Pull that shared state into its own component or pass it through explicitly. One thing beginners miss is that composition is not purely additive. The act of combining two correct components can expose interaction bugs that neither component shows alone. I learned this the hard way with a logging middleware that worked fine in every unit test but corrupted JSON output when composed with a response serializer. The middleware added a trailing newline. The serializer expected clean bytes. Both were correct in isolation. The composition broke the contract. The workaround was to define a byte-level invariant for the pipeline and assert it after every composition step.

When Composition Fails

Composition is not a silver bullet. It breaks down in scenarios where components genuinely need to reach across boundaries to function correctly. Performance-sensitive loops that require shared memory layouts are one example. Event-driven systems with circular dependencies are another. If your architecture forces you to thread state through six layers just to pass a configuration value, you are not composing well. You are shuffling data around a poorly designed graph. Another failure mode is over-composition. People sometimes split modules into tiny pieces because the theory says it is better. The result is a project where reading the code takes longer than writing it because you must open twenty files to understand a single workflow. That is not composition. That is fragmentation with extra steps. If you hit a case where compositional design is forcing too much indirection, consider whether a monolithic component is actually the better choice for that slice of the system. Not everything benefits from separation. High-frequency internal helpers that are always used together often perform better and are easier to maintain when they live in the same file.

A Real-World Walkthrough

Here is how I approach composing a data transformation pipeline. The pipeline reads records from a source, normalizes them, validates constraints, enriches with external data, and writes to a destination. Each step is a function that accepts a record and returns a record, plus an error channel for failures. The first step is normalizing field casing and stripping whitespace. A naive implementation would mutate the record in place. That works until you need to replay a step or diff two versions. I prefer returning a new record object so each stage is pure. The cost is slightly more memory allocation, but it makes testing trivial and eliminates a class of mutation bugs. The second step is validation. Here I learned to separate schema validation from business-rule validation. Schema checks whether the record has the right shape. Business rules check whether the values are acceptable given domain constraints. Mixing them causes confusing error messages and makes it impossible to skip business rules during debugging. I keep them in separate functions and compose them with a validator combinator that runs both and collects errors into a single list.

The Language of Composition: Reading, Writing, Rhetoric (Hardcover) by Renee H Shea, Lawrence ...
The Language of Composition: Reading, Writing, Rhetoric (Hardcover) by Renee H Shea, Lawrence ...

The third step is enrichment. This is where things get messy because enrichment usually depends on external services. I wrap the HTTP call in a retryable component with a circuit breaker and a timeout. Without those guards, a slow downstream service cascades into every upstream consumer. I also add a caching layer keyed by record ID so repeated requests for the same enrichment do not hammer the provider. The cache TTL is short because enriched data can become stale quickly in our domain. The fourth step is writing to the destination. At this point I compose all previous steps into a single pipeline function. The pipeline function takes a stream of raw records and emits a stream of persisted records. Error handling happens at the edges. Failed records go to a dead-letter queue instead of stopping the entire pipeline. I also add a metrics counter for each stage so I can see which step is introducing latency or errors. When this pipeline first shipped, the enrichment step caused intermittent failures because the cache key did not account for regional variations in the provider response. Two records with the same ID could return different enrichment data depending on the request origin. The fix was to include the region in the cache key. I should have caught that earlier, but it required seeing the failure in production under real traffic patterns. Local tests never reproduced it because the mock provider returned the same data regardless of headers.

Tips That Actually Matter

Write component tests before you compose anything. I know that sounds obvious, but most teams skip this when they are under deadline pressure. Skipping it saves an afternoon and costs three days later when a composition bug surfaces in staging. Name your composition operators clearly. A function called compose does not tell you what is being composed. A function called buildValidationPipeline tells a human reader something useful. Naming is a cheap way to encode intent. Keep a map of which components are stateless and which are stateful. Stateless components compose freely. Stateful components require ordering and often lockstep execution. Mixing them without tracking causes concurrency bugs that are nearly impossible to reproduce reliably.

When you refactor, test composition at every step. Do not refactor ten components and then compose them and hope nothing broke. Refactor one, compose it, verify, then move to the next. This methodical approach catches regressions before they accumulate. For writers, the same principles apply. Define your thesis as a single statement. Break supporting points into independent sections. Each section should hold up on its own. If a paragraph depends on information introduced three sections earlier, move that information closer or repeat it briefly. Readers do not reward you for making them re-read.

The Language of Composition : Reading, Writing, Rhetoric by Lawrence Scanlon 9780312676506| eBay
The Language of Composition : Reading, Writing, Rhetoric by Lawrence Scanlon 9780312676506| eBay

Tools I Use

I do not rely on any single framework for compositional design. The pattern is language-agnostic. I use standard unit test runners for each environment, a linter to enforce consistent interfaces, and a small harness script that runs composition smoke tests after every build. The harness takes a fixed set of sample records through the pipeline and checks that the output matches expected shapes and constraints. If you are looking for libraries that make composition easier in a specific language, the right choice depends on your stack. In TypeScript, libraries like Remeda and Ramda provide composition utilities. In Python, toolz and fn.py offer similar primitives. In Go, functions are already composable by default because closures are first-class citizens. No extra library needed. For text composition, I use a simple outline formatter that converts markdown headings into a nested structure. It helps me see at a glance whether my sections are balanced or whether one argument is dragging on without adding new information. The tool is basic. It works because it does exactly one thing well.

What To Avoid

Do not confuse composition with inheritance. Inheritance creates deep hierarchies that are fragile to change. Composition creates flat structures that are easier to modify. If you find yourself overriding methods to change behavior, you are probably using inheritance when composition would be cleaner. Do not compose components that share mutable global state. Shared globals are the quickest path to non-deterministic behavior. Pass state explicitly or encapsulate it inside a component where it belongs. Do not optimize for composition at the expense of readability. If your composed pipeline is shorter but twice as hard to understand, you have made a poor trade. Clarity wins. Brevity is secondary.

Finally, do not treat composition as a replacement for good architecture. You can compose a terrible design just as easily as a good one. The pattern amplifies whatever structure it sits on. Make sure the structure is sound before you start connecting pieces.

The Language of Composition by Renee H. Shea, Lawrence Scanlon, Robin Dissin Aufses
The Language of Composition by Renee H. Shea, Lawrence Scanlon, Robin Dissin Aufses