Simple Is Not Small: Why Unix Pipelines Are Coupled, Not Simple

Simple Is Not Small: Why Unix Pipelines Are Coupled, Not Simple

In this post, the author argues that simplicity and smallness are often conflated, using Unix pipelines as an example of small but coupled tools. He contrasts this with Clojure's decoupled approach, where data and behavior are separated, and discusses how large programs like Google Drive can feel simple to users. The post concludes that simplicity should always be prioritized, even if it requires more effort.

Unix tools are small but they are not simple.
  1. getnormality

    Strong resonance with the famous essay "The Rise of Worse is Better" [1], which contrasted the (better) "MIT/Stanford style of design" with the (worse) "New Jersey approach".

    MIT/Stanford:

    > Simplicity -- the design must be simple, both in implementation and interface. It is more important for the interface to be simple than the implementation.

    New Jersey:

    > Simplicity -- the design must be simple, both in implementation and interface. It is more important for the implementation to be simple than the interface. Simplicity is the most important consideration in a design.

    TFA maps "simplicity" to "MIT/Stanford simplicity" (simplicity for the user) and "smallness" to "New Jersey simplicity" (simplicity for the developer).

    I wonder if the root of the tension between the two schools comes down to the ambiguity of the user/developer distinction. Developers are also users. Simplicity of implementation is helpful to developers when they are working directly on implementation, while simplicity of interface is helpful to developers when they are using other developers' work.

    [1] https://dreamsongs.com/RiseOfWorseIsBetter.html

  2. Twey

    > The reason for this is that in Rust, a struct couples type-checking to a fixed data representation. You can't get one without the other.

    > Clojure decouples data representations from type checking.

    This is funny to me because seen from the other side, (this) Clojure couples runtime type information to data structures: you're no longer allowed to define a data structure that doesn't have some runtime type information attached. A fixed static structure is just the consequence of not adding dynamic type information.

    Meanwhile in Rust you can get type-checking ‘without’ a fixed structure by using trait objects.

  3. zkmon

    Complexity (the opposite of simplicity) has nothing to do with the size of a program, but usually there is a high chance that a large program is more complex than smaller one, purely because the complexity multiplies, not just adds up.

    A single regex line could be far more complex than a 100-line java program.

  4. pianopatrick

    I've been thinking about a new AI based dimension to this. If your program is split into smaller decoupled "modules" then all the code for each "module" can fit into an AI context window. In this way the AI can have all the context to edit a "module" by just loading all the code for that "module". You would not need things like vector search as much. If we assume 20 tokens per line of code and the AI context window is 100k to 1M tokens, then that would argue for having "modules" between 5,000 and 50,000 loc, depending on which AI model you are using.

  5. hankbond

    Great piece, very straightforward examples, although I did have to squint for quite a while to grok the Closure portion.

    I am currently building a piece of very modular software and it has been the hardest-to-design project of my entire career. I would never be allotted this amount of time-effort at any job I have held to make something this robust and clearly defined. Many aspects of this project have taken 3-5 rounds trying-and-trashing to get an abstraction that is uncomplicated.

    This is precisely why vibe coding is so successful for building tiny isolated scripts, and so disastrous for anything else. It's just really dang hard to build something large and simple.

More from this day

2026-09-07