A boundary rule stolen from urban planning: sort by rate of change, push irreversibility down, point dependencies at the slow layer.Architects across different subject areas - like civil engineers, urban planners, and hardware designers spent decades on problems we keep rediscovering every few years under new names. The habit of looking sideways at “older” or, simply, “other” disciplines and asking what they have already solved has done no less for my architectural instincts than any single framework. Learn to spot the borrowings and steal them well, and you get better at the job in a way that outlasts whatever stack you're on this year.This isn't my first time leaning on that habit in public. I once wrote about how mental models and mathematics helped me design a hotel room-cleaning scheduler — the solution only clicked after I looked at how IMDb ranks its Top 250 and how CSS computes specificity. Those analogies came from other corners of software. This time, let's go further afield!This post has two goals. The small one: a specific rule for where to put your architectural boundaries. The bigger one: convincing you that hunting for these cross-disciplinary steals is a skill worth building, and showing how to do it without sliding into vibes.TL;DR: We draw software boundaries by function, by team, or by domain noun. Almost never by how fast things change. So our fastest code (feature flags, experiments, vendor integrations) ends up wired straight into our slowest, least reversible layer (the data model, the money ledger, public contracts). Stewart Brand's pace layers names the fix, and Parnas named it in 1972: sort by rate of change, push irreversibility down, point dependencies at the slow layer. Draw the boundary where the change-rate breaks, not where the org chart does.Picture a one-week marketing experiment that turns into a database migration.A trivial test: show two versions of a checkout promo to 10% of traffic, see which one converts better. But the experiment framework needs to record which variant each order saw. Head-on path: a new column on the orders table, promo_variant. One 40-line migration later, the experiment has a home.Now think about what just happened. A disposable test, the kind that should live and die, reached all the way down and changed the most permanent structure in the system. The orders schema is the thing you migrate at your peril. Finance reconciles against it. Three other services read it. And a promo banner got to write into it.Nobody would build that on purpose; the boundary was just drawn in the wrong place. Why does a thing that changes weekly get to touch a thing that changes yearly? Who decided the fast layer gets to write into the slow one?I've got one lens for that. It comes from outside software.🏙️ The lens: layers that move at different speedsIn 1999, Stewart Brand described something he called pace layers: the idea that a durable system is really a stack of layers, each moving at its own speed.For a civilization, he named six, fast to slow: fashion, commerce, infrastructure, governance, culture, nature. Fashion changes by the season. Nature changes over millennia. The layers aren't ranked by importance. They're ranked by speed.The part that matters is Brand's own summary:Fast learns, slow remembers. Fast proposes, slow disposes… Fast gets all our attention, slow has all the power.And the line I'd tape to a monitor:Some parts respond quickly to the shock, allowing slower parts to ignore the shock and maintain their steady duties of system continuity.Read that again. The fast layers exist partly to absorb shocks so the slow layers don't have to. Resilience lives in the separation between the rates, not in any single layer.Your boundaries are the things you choose, on purpose. So I'll borrow three ideas and drop the rest: rate of change defines a boundary, coupling has a direction, and fast layers absorb shocks for slow ones.A small confession before we move on. I've bumped into hexagonal architecture many times throughout my career, and for years it stayed an abstract mystery: I could redraw the diagram, but I couldn't find a specific application for it in my own systems. What finally made ideas like that click was drawing analogies from disciplines I actually understand, the ones that live in the physical world. Hexagonal architecture shows up again later in this piece — see if the street grid makes it land differently for you.🧱 The rule we already hadDecompose a system by rate of change and you haven't borrowed from urban planning. You've rediscovered David Parnas.In 1972, Parnas said: don't split a system along its flowchart. Instead, start with the design decisions "which are likely to change," and hide each one behind its own module.Decompose by what's likely to change. That's rate-of-change decomposition, written down in 1972, decades before Brand's book.That idea aged well. Uncle Bob later folded it into the Single Responsibility Principle and restated it in plain words:Gather together the things that change for the same reasons. Separate those things that change for different reasons.Put that next to Brand. Gather what changes together. Separate what changes at different rates. One sentence is about civilizations, the other about classes.That's why this lens is safe to borrow. Two fields found the same rule independently, which is about the strongest evidence you can ask for. And software got there first.🛣️ Why we draw the boundary in the wrong placeBecause in practice we don't sort by rate of change. We sort by function ("the reporting module"), by team ("that's Platform's service"), or by domain noun ("everything about Orders goes in Orders").That last one is how a weekly experiment lands in a yearly schema. Because the experiment is about orders, it goes where orders live: the slowest, most-reconciled, most-depended-on layer in the system. The noun was the wrong axis.A downtown street grid is the slow layer of a city. You lay it down once and leave it for a century. The shops, signs, tenants, and window displays on top of it change constantly. Now imagine a city that repaved Main Street every time a boutique changed its window display. Tearing up the roadbed to swap a poster. You'd call that town insane.A schema migration for a one-week banner is that town. The grid and the window change at different rates, and a healthy city keeps them on opposite sides of a very firm line.🎯 The rate-of-change ruleSo here's the discipline, in three beats. I run it whenever I'm deciding where a boundary goes.Sort by rate of change. Before grouping things by what they are, group them by how often they actually change. And software has an edge a city planner would kill for: you can measure this. Your commit history is the data. A file you touch every sprint and a table you've migrated twice in three years are not in the same layer, no matter how related they look on a diagram.Push irreversibility down. The slow layer is wherever the hard-to-reverse decisions live: the data model, how you represent money, public API contracts, ID schemes. The stuff migrations are scary for. Put those decisions there on purpose, keep them small, and guard them behind a stable interface.Point every dependency at the slow layer. Volatile code may depend on stable code. Stable code must never depend on volatile code. This is Uncle Bob's Stable Dependencies Principle ("depend in the direction of stability") and it's Cockburn's hexagonal architecture, with a stable core and volatile adapters pointing inward. Or in Brand's words: fast proposes, slow disposes.One line to remember it: sort by rate of change, push irreversibility down, point dependencies at the slow layer.Example 1: evaluate feature flags in the fast layerFeature flags are the fast layer. They flip daily. Experiments start and end inside a sprint. If anything in your system lives in Brand's "fashion" register, it's this.The anti-pattern: wiring flag evaluation into the backend core. Deep service logic, often the code closest to the data model, branching on if (flags.promoVariantB). Every flag your marketers dream up is a code change in the slow layer. And the evaluation logic usually reads against the same data model the core is supposed to protect. That's how you get a promo_variant column.I've paid for a variant of this mistake myself. On a scheduling product I work on, we were evolving the domain model: an appointment used to be a single block with one start and one end time, and we were splitting it into individual services, each with its own times, so we could book specific resources for exactly the duration of each service. A legitimate slow-layer change. But we gated the transition with feature flags evaluated everywhere — client app, edge, backend — baking variant logic into existing endpoints and functions at every level. Two things bit us. The logic got overcomplicated fast, since every layer grew its own if/else forest. And rollouts got fragile, because each layer cached and refreshed flags on its own schedule; mid-rollout, the client could believe in the new model while the backend still served the old one. Same flag, three answers.The fix: evaluate the flag once, in one layer, as high up as you can — the client or the edge. One evaluation point means one source of truth during rollout; everything below it receives data, not flags. Keep the backend exposing stable capabilities, not per-experiment branches. The core knows how to "apply a promotion of type X." It has no idea that X is being A/B tested against Y under a flag that expires Friday.// Anti-pattern: the experiment reaches the slow layer.// Backend core, sitting next to the data model, branches on the flag// and persists the variant onto the orders row.function priceOrder(order: Order): Money { if (flags.isEnabled("promo_variant_b", order.customerId)) { order.promoVariant = "B"; // new column on the yearly schema return applyPromoB(order); } return applyStandardPricing(order);}// Fix: branch in the fast layer; the core takes a stable capability.// Fast layer (client/edge) — owns the experiment, expires Friday.const variant = experiments.assign("promo_variant", userId); // "A" | "B"const promo = variant === "B" ? PROMO_B : PROMO_A;// Backend core — knows nothing about the experiment or the flag.function priceOrder(order: Order, promo: PromotionCode): Money { return applyPromotion(order, promo); // no flag, no promo_variant column}Example 2: the API gateway as a shock absorberRemember Brand's line about fast layers absorbing shocks so slow layers keep their "steady duties of system continuity"? That's an API gateway, done right.Upstream of the gateway, everything churns. Clients ship weekly. Auth schemes rotate. Rate limits get tuned. A partner's endpoint moves. Downstream, you want your domain services to evolve on their own slower schedule, not redeploy because a mobile client changed a header.The gateway sits on the seam and eats the fast-moving shocks. It handles the churning concerns (protocol versions, auth, throttling, request shaping) and hands the services a stable contract underneath. That's claim three from the lens. The fast layer takes the hit so the slow layer keeps going.Two things I check for here, because this one gets cargo-culted:A gateway only absorbs shocks if its downstream contract is actually stable. Forward every upstream change straight through and it stops being a shock absorber; it's a slow layer in fast-layer clothes, and all you've added is a hop.The gateway earns its keep by being slower than the clients above it and no faster than the services below it. If it churns as fast as the clients, it has joined the fast layer and stopped protecting anyone.All of the value is in the rate separation; none of it is in the box.# API gateway route — the seam that absorbs fast-moving concerns.- path: /checkout # These churn constantly and terminate HERE, never reaching the service: auth: oauth2 # scheme rotates rate_limit: 100/min # tuned weekly accepts: [client-v1, client-v2, client-v3] # three client versions in… # …one stable contract out; downstream evolves on its own schedule. upstream: checkout-service contract: checkout.v1Example 3: let the fast layer propose, never writeBack to the orders table. Done right this time.The mistake was letting the experiment framework write straight into the ledger. The fix is a stable contract, call it OrderLedgerPort, that the ledger owns and the fast layers can only call through.Experiments live entirely in the fast layer. When one wants to influence an order, it proposes through the port. It calls a defined capability, passes what the contract allows, and the ledger decides what to persist.And the dependency points one way only. The experiment imports the port. The ledger imports nothing from the experiment. Delete the whole experiment framework tomorrow and the ledger wouldn't notice.// SLOW layer — the ledger owns this contract. Changes ~yearly.interface OrderLedgerPort { recordAdjustment(orderId: OrderId, adj: PriceAdjustment): void;}// FAST layer — depends on the slow contract, never the reverse.// It proposes; the ledger decides what to persist. Expires Friday.class PromoExperiment { constructor(private ledger: OrderLedgerPort) {} onCheckout(order: Order): void { const adj = this.pickVariant(order); // experiment logic lives here this.ledger.recordAdjustment(order.id, adj); // one-way call, down the gradient }}// The ledger imports nothing from PromoExperiment.This is just hexagonal architecture plus the Stable Dependencies Principle, applied on purpose. But the rate-of-change framing tells you the one thing the patterns don't: where to put the port. It goes exactly where the change-rate breaks, between the weekly experiments and the yearly ledger, and the dependency crosses that line toward the slower side.Swap the fast layer? Fast-layer-only change. The roadbed stays put while the window display changes.🤨 "Isn't this just SRP with a Stewart Brand sticker on it?"Fair. Let me meet it head-on instead of dodging.Yes, the destination is mostly the Single Responsibility Principle and the Stable Dependencies Principle, and both predate this post by decades. If you already design strictly by SRP and SDP on instinct, this lens tells you nothing new.For the rest of us, it adds three things the principles leave fuzzy:A measurable criterion. Uncle Bob says the "reason to change" in SRP is really about people. Honest, but hard to use at 4pm on a Thursday. Rate of change is sitting in your git history. You can chart it and refactor against it.Directionality with a reason. SDP says depend toward stability. Pace layers says why that's resilient and not just neat: it lets the fast layers absorb the shocks.A systemic frame. SRP is about one module's cohesion. Pace layers is about the whole system's health. The difference between "this class is tidy" and "this architecture bends instead of breaking when the fast parts thrash."Same rule, in a frame that actually motivates the discipline.🧘 What software has that cities don'tI'll close with the honest disanalogies, because pretending the transfer is exact is how you get dunked in the comments.A city's pace layers are descriptive. Brand observed them. Nobody assigned the street grid its rate of change. Our layers are prescriptive: we choose where the boundaries go. That's a burden (nothing enforces it for us) and a gift (we can put the boundary in the right place on purpose).And two things software has that concrete-and-rebar doesn't. We redeploy continuously, so our "slow" layer is slow by choice, not by physics; a bad boundary is fixable, if painfully. And we can measure rate of change from history instead of guessing at it. Brand's lens hands us the principle, and our tools let us apply it with a precision he could only dream of for a city.None of this is a silver bullet, as usual. Rate of change is one axis to weigh against cohesion, team lines, and plain domain sense. Sometimes they pull apart and you iterate to find the balance; every system's layers are its own.But the next time a one-week experiment asks to write into a ten-year schema, you'll have a name for the discomfort. And a rule for what to do about it.Draw the boundary where the change-rate breaks, not where the org chart does. Fast proposes, slow disposes. Make your dependency arrows agree.