Lessons from building web applications since 2013

LoweringComplexity

Allow the minimum complexity reality requires.

The web gives us extraordinary reach. Everything else in a system should point to a real constraint, a product promise, or a failure the team refuses to tolerate.

Read the essay
SpaceX Raptor 1 rocket engine, showing its dense network of pipes and components.
Raptor 1. Complexity can be justified; it still has to name the reality that forces it. Photo: Brandon De Young, CC BY-SA 4.0.

01

The premise

The web’s gift—and its invoice

The web is an extraordinary environment. Arbitrary, untrusted code arrives on demand, opens on nearly any machine, and is reasonably safe to run.

A product is easy to share. A programmer can get a satisfying visual result in minutes. That makes the web perhaps the easiest place to prototype an application.

It also gives the ecosystem a powerful “worse is better” instinct: make the rough thing available, let it spread, and improve it later. That instinct helped the web win. It also left us with layers of machinery—webpack-era build systems, Flux-era state management, and countless conventions built on top of conventions.

The legitimate invoice for reach

  • Different screens and densities
  • Mouse, touch, keyboard, and pen
  • Accessibility and internationalization
  • Unreliable networks and caching limits
  • Crawlers, bots, and link previews
  • Old browsers and low-end devices
  • Media formats and decoding
  • Permissions and sandboxing

Some complexity is the honest price of that reach. The mistake is not paying the invoice. The mistake is paying invoices that belong to somebody else.

The browser should provide distribution and capabilities.

Applications should produce experiences.

02

Define complexity

Count behaviors, not files

Complexity is the number of ways a system can behave.

That definition is more useful than counting files, lines, dependencies, or abstractions. The real cost is the number of facts and possible behaviors that must stay coordinated before a change is safe.

Consider a harmless-looking type:

BeforeSeven independent dimensions
type User = {
  is_signed_in: boolean

  username: string | null
  display_name: string | null

  is_subscribed: boolean
  is_deleted: boolean
  is_banned: boolean
  is_vip: boolean
}
27=128

possible states, before counting what the strings contain

Those states include combinations the product may never intend: a deleted VIP, a banned subscriber, or a signed-out user with a username. If the type permits those combinations, every consumer must either decide what they mean or improvise accidentally.

Handling every possible state is responsible. Writing 128 conditionals on every route is not. The better move is to remove states that should never exist and encode the intended alternatives once.

AfterMake invalid states impossible
type User =
  | {
      status: "authenticated"
      username: string
      display_name: string
    }
  | {
      status: "anonymous"
    }

The union makes a product decision structural. Consumers no longer need to remember a convention about which combinations are valid. Prefer structures that produce the right behavior over conventions that ask everybody to remember it.

03

Principle 1

Reality sets the price

Every complex system should be able to name the pressure that makes a simpler alternative inadequate.

01A platform limit
02A product promise
03A failure the team refuses to tolerate

Then interrogate it. Does that pressure apply to us now? Actually? Could the experience change instead? Could a deeper layer own the guarantee?

Our actual environment

Millions of users.

Media-heavy, real-time creative flows.

A long tail of very large creators.

Roughly seven web engineers with broad, independent ownership.

Those facts justify careful media systems, predictable recovery, and serious performance work. They do not justify architecture designed for a thousand frontend engineers changing one application in parallel. Organizational scalability is a real constraint—but only when it is actually yours.

Keep forced machinery narrow

Server-side rendering is a useful example. Discoverability and link previews require pages that crawlers can understand. Midjourney runs a Bun server for marketing pages and image previews, while the bulk of the application stays client-side.

The constraint is named, its effect is narrow, and the machinery remains proportional to its reason. If the pressure disappears, the machinery should be easy to remove.

Specialize around the product, scale, environment, and team you actually have.

04

Principles 2 and 3

Global facts need one authoritative answer

Distributed implementation is fine. Distributed authority is expensive.

Navigation, z-order, shared state, resource loading, input, and recovery order are global facts. Giving each one an authoritative home does not mean hiding the application inside one enormous abstraction. It means deciding where disagreement is resolved.

A / Router

The browser is an input, not a second owner.

At startup, parse the URL into a typed route. Back and forward navigation return as explicit input events. The application owns the decision; the address bar remains its shareable representation.

This is not an argument against deep links or browser history. It is an argument against two continuously competing sources of truth.

B / Z order

Z-index is global whether the code admits it or not.

let d = 1

export const loft = d++
export const lightBox = d++
export const sideBar = d++
export const composer = d++
export const contextMenu = d++
export const tooltip = d++

d = 9999
export const modalBase = d++
export const errorBoundary = d++
export const toaster = d++

Why is one surface above another? This list decided it.

C / Shared state

Write down the true shape.

Roughly 90 percent of our application state is global and does not change over time. Product decisions make most rendered records stable. We fetch information that must render together together, load shared facts once, keep their authority global, and derive local views from them.

Multiple providers often become an implicit dependency graph. Their nesting looks tidy until dependencies point backward and a cycle reveals that the tree was never the real shape. Flattening the graph does not mean a careless global bag: data structures, lifetimes, mutation paths, and derived views still need names.

05

Know the bounds

Prefer predictable whole-path behavior

We re-render the whole application on scroll. In some places, on mouse move.

That sounds reckless if average work is the only standard. Our standard is predictable frame behavior across the whole experience, including unusual states, large accounts, custom hit testing, scrolling, and moving interface elements.

A deliberately provocative rule

Do all the work, all the time.

If it is slow, fix it until it is always fast enough.

This is not universal advice. It is one way to prevent rare combinations from secretly becoming much slower because a memoization boundary or incremental path stopped behaving as expected. The complete path exposes the tail instead of optimizing only the friendly path.

Good engineering is not merely producing a good average. It is knowing the bounds.

06

Networking is complex

Make execution order, ownership, and lifetime visible

Websocket reconnection is a global ordering problem. Turn it into a written protocol.

  1. 01

    Preserve a durable resume cursor or sequence.

  2. 02

    Reconnect and subscribe without opening a gap.

  3. 03

    Fetch a sequence-tagged snapshot of running and completed work.

  4. 04

    Restore progress subscriptions.

  5. 05

    Reconcile idempotently and reject stale progress.

A claim such as “we never miss anything” is only defensible when the backend provides the cursor, there is no gap between snapshot and stream, and reconciliation is idempotent. Those preconditions are the engineering.

During a failure, we need to know what ran, in what order, which state it owned, and how long that state was allowed to live. Quantify tolerances, failure, and recovery. Imprecision is sometimes acceptable; unexplained imprecision is not.

07

Principle 4

Abstract machinery, not meaningful decisions

An abstraction becomes dangerous when its interface makes the caller believe the problem has a simpler shape than reality.

A websocket capability may hide timers, retries, and socket APIs. It should not hide the policy for stale data, cascading failure, or reconciliation.

Phase 1prepare(…)

Fonts, DOM, measurement machinery

Phase 2layout(…)

Geometry, ordering, product placement

Pretext can hide DOM access, font loading, and measurement machinery. It deliberately keeps preparation and layout visible, along with the geometry, ordering, and placement that the product still needs to decide. That is the line: hide mechanism, expose meaning.

Prefer capabilities over frameworks. A focused library or bounded service can solve one difficult boundary without deciding the shape of the entire application. Do not build a website around the way React—or any framework—wants the code organized. Build the experience users need, and go around the framework when the experience requires it.

A screen is a state. The experience lives in the transitions between states.

08

The final ten percent

A prototype asks a question. A product makes a commitment.

The web is unusually good for prototyping. That is a gift, but prototyping and committed product engineering are different concerns.

A prototype proves that something can happen. Engineering makes it keep happening.

Before the prototype becomes a product, name:

  • Data

    The structures, mutations, and places where state is stored.

  • Standards

    The performance bounds the application must meet.

  • Depth

    The guarantees a backend or deeper layer can enforce.

  • Edges

    The cases the happy prototype never encountered.

  • Failure

    Every error state, tolerance, and recovery path.

  • Ownership

    Who owns each decision and how long its state lives.

Engineering begins where multiple reasonable requirements cannot all win. The work is choosing the tradeoff deliberately, then finishing the unglamorous final ten percent that makes the commitment dependable.

09

Looking forward

Let AI pay mechanical complexity

AI lowers the cost of synchronized edits, adapters, boilerplate, migrations, and other mechanical work. That changes some architectural tradeoffs.

If a model can keep explicit code synchronized, an abstraction that existed mainly to reduce typing may no longer be worth the conceptual cost. Two intentionally unfinished experiments follow from that instinct:

Experiment 01

One initial request instead of roughly sixteen

Reduce request ordering and partial-loading regimes without ignoring payload size, caching, or time to useful rendering.

Experiment 02

One geometry system on controlled creative surfaces

Test whether explicit positioning can replace a split between CSS and JavaScript without sacrificing accessibility or responsive text.

These are experiments, not doctrine.

LLMs can pay mechanical complexity.

They should never conceal conceptual complexity.

Models do not decide state ownership, ordering, tolerances, failure policy, or recovery for us. Those decisions often reveal their consequences years later or only in a small tail of users. Engineers still own the operating model.

10

Principle 0

Know yourself

Γνῶθι σεαυτόν

In engineering terms: be honest about the complexity you actually have to tackle. Understand the product, environment, team, and failures you cannot tolerate. Refuse both imaginary constraints and comforting denial of real ones.

  1. 01

    What constraint, product promise, or intolerable failure forces this complexity?

  2. 02

    Does that pressure apply to us now?

  3. 03

    Can we change the experience instead?

  4. 04

    Can a deeper layer own the guarantee?

  5. 05

    If it is unavoidable, how small can its boundary be?

Games

Make time, memory, and order explicit.

Interaction

Care about what happens beneath the finger.

Hardware

Estimate, define tolerances, and recover in an imperfect world.

Borrow the standards that travel: visible order, bounded resources, back-of-the-envelope math, explicit tolerances, deliberate recovery, and care in the transitions where the experience actually happens.

Lowering complexity is an organizational technology. It lets a small team own big pieces, work independently, deploy calmly, and keep systems understandable for years.

Build the experience.

Name the pressure.

Pay only what it costs.

Sources and credits

  • “Worse Is Better”, Richard P. Gabriel.
  • Raptor 1 photograph by Brandon De Young, licensed CC BY-SA 4.0, via Wikimedia Commons.
  • The Pretext example comes from Cheng Lou’s work.
  • Architecture and networking examples are adapted from practices discussed by the Midjourney web team.