002

Less Is More

What Mies van der Rohe knew about code: why DRY is hurting you, abstractions are couplings in disguise, and nobody ever defined ‘premature.’

“Less is more.” — Ludwig Mies van der Rohe
Seagram Building, Park Avenue, 1958
Seagram Building, Park Avenue, 1958. Photo: Noroton, public domain, via Wikimedia Commons.

Mies van der Rohe didn’t coin that phrase as a decorating tip. It was an engineering claim. In 1958 he finished the Seagram Building on Park Avenue — steel, glass, proportion, and nothing else. No gargoyles, no cornices, no flourishes announcing the architect’s cleverness. Every element in that building either carries load or lets in light. His critics called it cold. Then everyone spent fifty years copying it badly, and the copies were worse for a revealing reason: the imitators couldn’t resist adding things. A flourish here, a clever angle there. They produced buildings that were more complicated and less good, and never understood why, because they’d missed the entire point. Every element must justify its existence. Most can’t.

I’ve been writing software long enough to believe the same thing about code — and to have learned it the expensive way.

Two ways to total an order

A few years ago I inherited a service. Small thing — it took orders, totaled them, sent them downstream. The kind of code you’d expect to read in ten minutes.

It took me two days.

Not because the logic was hard. Because I couldn’t find it. The solution opened with eleven projects, an Abstractions folder, and a dependency graph that looked like a subway map. Somewhere in there, an order was being totaled. I just had to figure out where.

What I was looking for was, in the end, this:

decimal total = 0;
foreach (var item in order.Items) {
    total += item.Price * item.Quantity;
}

What I had to walk through to find it was this:

public interface IPriceableItemVisitor<TContext> where TContext : IAggregationContext {
    void Visit(IPriceable item, TContext context);
}
public class OrderTotalAggregationStrategy : IAggregationStrategy<Order, decimal> {

    private readonly IPriceableItemVisitorFactory _visitorFactory;
    private readonly IAggregationContextProvider _contextProvider;

    public OrderTotalAggregationStrategy(
        IPriceableItemVisitorFactory visitorFactory,
        IAggregationContextProvider contextProvider) {
        _visitorFactory = visitorFactory;
        _contextProvider = contextProvider;
    }

    public decimal Aggregate(Order order) {
        var context = _contextProvider.CreateContext<MonetaryAggregationContext>();
        var visitor = _visitorFactory.Create<MonetaryAggregationContext>();
        foreach (var item in order.Items.OfType<IPriceable>()) {
            visitor.Visit(item, context);
        }
        return context.Result;
    }
}

…plus the factory, the context provider, the DI registrations, and four more files I’m sparing you.

Here’s the part that matters: the author wasn’t incompetent. Quite the opposite. Every pattern was applied correctly. The code was extensible, decoupled, testable — and nearly impossible to read. It was, in other words, exactly what we train senior engineers to write.

Now the uncomfortable question. Which version gets nodded through design review as “senior” code? And which one would you rather debug at 3 AM, when the total is off by one cent and money is leaking?

We all know the answer to both questions, and the answers are not the same. That gap — between what we praise and what we’d actually want to inherit — is what this article is about. (The story of that service has an ending, by the way. I’ll get to it.)

Look at that second sample through Mies’s eyes. It’s all gargoyles. The visitor pattern, the strategy interface, the context provider — none of it carries load. The load is total += item.Price * item.Quantity. One line, buried in there like a steel beam wrapped in decorative plaster. Everything around it exists to make the code look like serious architecture, not to make the building stand up.

Simplicity is the senior skill

Barcelona Pavilion (Mies van der Rohe & Lilly Reich, 1929)
Barcelona Pavilion (Mies van der Rohe & Lilly Reich, 1929). Nothing here is decoration. Nothing is missing. Photo: Christian Gänshirt, CC BY-SA 4.0, via Wikimedia Commons.

Somewhere along the way we decided simplicity is a junior trait. Juniors write loops; seniors write frameworks. Interfaces, factories, generic constraints three levels deep. We confuse sophistication with indirection, and we reward the confusion in every design review.

But that abstracted monstrosity is not what experienced code looks like. It’s what insecure code looks like — code that needs to demonstrate effort. The four-line loop is what code looks like after someone fully understood the problem, and understanding the problem is the actual senior skill. Anyone can wrap a loop in five layers. Distilling five layers back down to a loop takes confidence and comprehension.

There’s a line often attributed to Pascal: “I would have written a shorter letter, but I did not have the time.” Simple code is the shorter letter. It costs more to produce and less to own — and “less to own” compounds, because code is written once and read hundreds of times, by people having worse days than you were having when you wrote it.

Mies didn’t arrive at steel-and-glass by lacking ideas. He arrived there by deleting every idea that didn’t survive interrogation. Simplicity was the output of his sophistication, not the absence of it.

DRY is hurting you

Now for the principle doing the most damage per syllable: Don’t Repeat Yourself.

DRY, as originally written, is about knowledge: every piece of business knowledge should have one authoritative home. Reasonable. But that’s not the version we practice. The version we practice is a lint rule against text: if two blocks of code look alike, extract them. Immediately. Reflexively.

Here’s the trap. Two pieces of code that look identical today are not necessarily the same thing. Say the checkout page calculates a discount, and the invoice PDF calculates a discount, and right now the two calculations are character-for-character identical. The text repeats. The knowledge doesn’t — these are two different concepts, owned by two different parts of the business, that happen to rhyme at this moment in time.

But the reflex kicks in. Someone extracts CalculateDiscount(). One function, two callers, design review applauds. What we actually built is a party wall: two buildings sharing one wall — fine, until they're renovated by different owners. Then every change on one side cracks plaster on the other.

The renovations arrive on schedule. Marketing wants checkout to round differently — invoices must not change, finance will riot. So: bool roundForDisplay. Then the German market needs different VAT treatment on invoices only: a DiscountContext enum. Then promotions stack differently in the cart than on paper: an options object. Eighteen months later this "shared" function has seven parameters, four code paths, and a comment that says // careful: invoices depend on this. Changing it requires understanding every caller. Nobody understands every caller. So nobody changes it, and the workarounds begin — which is how you end up with CalculateDiscountV2, and now the knowledge lives in two places anyway, except worse.

You’ve built the most dangerous artifact in software: a function everyone depends on and no one understands. In the name of not repeating yourself.

Run the tape back. If those two code blocks had stayed separate, each business change would have been a five-minute edit with a blast radius of one file. The duplication was never the problem. It was coincidental similarity — same shape, different thing.

Now, the honest objection — because duplication has a real failure mode too. We’ve all seen it: a bug fixed in 39 of 40 copies, the tax table updated everywhere except the one report nobody remembered. That trauma is legitimate. But look at what defines that scenario: the fix had to be identical everywhere. And that is precisely the test for knowledge. If a change must propagate to every copy simultaneously, those copies were one piece of knowledge wearing forty costumes — unify them; that’s exactly what DRY is for. If each site is allowed to evolve independently, it was never one thing, and merging it builds the party wall. The failure cases of duplication and of deduplication don’t overlap — they sit on opposite sides of the same question: is this one piece of knowledge, or two that currently rhyme?

For the ambiguous cases, Sandi Metz gave us the tiebreaker: prefer duplication over the wrong abstraction. The asymmetry is brutal. Duplication is cheap to fix later — when a genuine shared concept emerges, the merge is mechanical. A wrong abstraction is load-bearing by Tuesday; removing it means excavating a foundation while the building is occupied.

So the actual rule: don’t repeat knowledge; ignore repeated text. Same shape is not same thing.

Abstractions are couplings wearing a disguise

We talk about abstraction as if it reduces coupling. Mostly it does the opposite, through two mechanisms that deserve names.

Gravity. Every abstraction is an invitation to reuse, and shiny generic code has gravitational pull. Build a flexible helper for one service and watch code start depending on it from places you never anticipated — places where it doesn’t quite fit, but it’s there, and it’s generic, so people use it. That’s how a utility written for one feature ends up underneath twelve, pulled into places it doesn’t belong.

Blast radius. A single change to a shared abstraction ripples through every consumer at once. You wanted to fix one behavior in one flow; you shipped a behavior change to eleven flows you weren’t thinking about, and you’ll find out which ones from the on-call channel. The more “reusable” a piece of code becomes, the more terrifying it is to touch — so it stops being maintained, ossifies, and everything routes around it. The most-shared code in a system is reliably its most-feared.

Compare the dumb duplicated version: each call site owns its logic, every change is local, every diff is reviewable by someone who knows that one context. Boring. Greppable. Safe. Boring is a feature. Boring is what Park Avenue looks like at the Seagram Building: calm on the surface, sound underneath.

And then there’s the speculative stuff — abstractions as bets on a future that never calls. The IRepository<T> with exactly one implementation, maintained for five years in case a second database materializes. The plugin system with one plugin. The configuration option that has never once been configured. We build these because abstracting feels like engineering and writing the plain version feels like not trying. Mies would have recognized it instantly: cornices on a server.

To be clear — this is not “never abstract.” It’s “abstract late, and only from evidence.” Let the rule of three do its job: the first occurrence is code, the second is coincidence, the third is a pattern — and by the third, you can finally see the actual shape of the shared concept instead of guessing at it. An abstraction earned from three real cases is structure. An abstraction invented for imaginary cases is decoration.

Nobody ever defined “premature”

The Longaberger headquarters, Ohio (1997)
The Longaberger headquarters, Ohio (1997). A $30 million office building shaped like the company’s basket, at 160× scale. The decorative handles contain heating elements so they never ice over. The ornament was carefully optimized; the company went bankrupt. Photo: Carol M. Highsmith, Library of Congress.

While we’re slaughtering sacred cows: “Premature optimization is the root of all evil.” Knuth’s line — the most weaponized sentence in software. It gets deployed in design reviews like a gavel, ending any conversation about performance before it starts. You asked how many allocations the serializer makes per request? Premature optimization. Case closed.

Two problems.

First, nobody quotes the rest of the paragraph. Knuth immediately added that we should not pass up our opportunities in the critical few percent that actually matters. The full thought is “don’t micro-tune cold code, do engineer the hot path” — which is just engineering. The bumper-sticker version deleted half the sentence and all of the judgment.

Second, and this is the part that amuses me: nobody ever defined premature. So the industry defined it operationally, by behavior. Want to know when thinking about performance stops being premature? When the incident starts. The practical definition of “premature” turns out to be “before the pager goes off.” Performance is premature right up until a customer complains, at which point it skips directly to overdue. There is no scheduled moment in between where it was simply due.

Now connect this to abstraction, because the two pathologies feed each other. The same culture that calls a benchmark “premature optimization” calls a factory of factories “good design.” Think about that asymmetry. We apply YAGNI ruthlessly to performance — don’t optimize until proven necessary! — and never to architecture, where speculation is praised as foresight. We treat speculative performance work as a sin and speculative abstraction as a virtue, when the abstraction is far more expensive to undo.

And abstraction doesn’t just postpone the performance conversation — it makes the conversation impossible. Look at the four-line loop from the beginning of this article. You can cost it by reading it: one pass over a list, two multiplications and an add per item, zero allocations. The code’s price tag is printed on its face. Now try costing the visitor-factory version: a context allocated per call, a visitor resolved per call, an OfType iterator with its own allocation, interface dispatch per item — and that's only what's visible in this file; the factory and provider hide more. Every layer of indirection launders the cost a little further, until no one can say what anything costs, so no one asks, so the question becomes "premature" by default.

That’s the hell the bumper sticker built: codebases where performance was never anyone’s job, because at every individual layer it was too early to think about it — and at the system level, too late. Simple code keeps you honest about cost the same way Mies’s glass walls kept the structure honest: there’s nowhere to hide.

Cleverness belongs in a box

Seagram facade
Seagram facade. The bronze I-beams carry nothing — fire code put the real structure behind them. Even the window blinds allow only three positions, so the surface stays calm. Photo: Gabriel Fernandes, CC BY-SA 2.0.

To head off the strawman: I am not against clever code. Some problems genuinely demand it — a lock-free queue, a hand-rolled serializer on a hot path where every allocation shows up in the histogram. I’ve written code I wouldn’t let a junior read unsupervised, and I’d write it again, because the benchmarks left no choice. That’s Knuth’s critical few percent, and it deserves real engineering.

The rule isn’t “never be clever.” The rule is containment: one file, one type, a small surface, a boring interface, and a test suite that nails its behavior to the wall. The caller sees queue.Enqueue(item) and never learns about the memory barriers inside. The complexity is paid once, in one room, by someone who chose to walk in. What's not fine is cleverness as a style — smeared across a codebase, leaking through every API, demanding that each reader be as clever as the author on the author's best day while having an average day themselves. That's not engineering; it's performance art with an on-call rotation.

The Seagram Building, by the way, is full of engineering virtuosity — Mies fought building codes and hung bronze on the facade to get the structure he wanted. All of it is hidden in the structure. The surface is serene. That’s the model: as clever as necessary on the inside, as boring as possible on the outside.

How the story ends

I promised an ending for the order-totaling service.

Edith Farnsworth House, Plano, Illinois (1951)
Edith Farnsworth House, Plano, Illinois (1951). One room. Glass walls. You can see everything from the front door. Photo: Victor Grigas, CC BY-SA 3.0, via Wikimedia Commons

After my two lost days, I did the disrespectful thing: I rewrote it. One project. The loop lives in the handler, where you can see it from the front door. A few hundred lines including configuration, finished in under a week — most of which went into proving the new version’s behavior matched the old one to the cent.

Code review had objections, and they were the objections you’d expect. “But it’s not extensible.” “What if we need different aggregation strategies later?” “You’ve coupled the calculation to the handler.” All true. We shipped it anyway.

That was years ago. Here’s the complete maintenance history since: two new requirements arrived — multi-currency support and a per-brand fee. Each one was an afternoon. Each diff was small enough that the reviewer could hold the entire change in their head, because the entire service fit in their head. Beyond that: zero incidents, zero refactors, zero meetings. Nobody talks about the service at all anymore — and in production, silence is the highest praise there is.

And the extensibility we deleted? This is my favorite part. None of the futures those eleven projects were preparing for ever arrived. No alternative aggregation strategies, no second visitor, no exotic context. Meanwhile, the changes that did arrive were ones the old abstraction hadn’t anticipated — multi-currency would have cut straight across its interfaces and been harder in the flexible version, not easier. The flexibility wasn’t just unused; it was pointed in the wrong direction. Speculative flexibility usually is, because the future doesn’t consult our interfaces before showing up.

That’s the whole article in one story. Write the obvious version first, and notice how often it’s also the final version. Duplicate without shame until repetition shows you the abstraction’s true shape — and unify ruthlessly the moment something proves to be one piece of knowledge. Keep cleverness in a box with a boring lid. Know what your code costs before the pager teaches you. Make the code so plain it’s almost disappointing — then enjoy the silence.

Mies spent a career deleting things, and everything that remained was stronger for it. He had a second saying, less famous than the first and possibly better advice for our profession:

“I don’t want to be interesting. I want to be good.”

Less is more. It held up in steel and glass for seventy years. It’s been holding up in that little service ever since.

$ EOF