Some sections of this post were written with the assistance of AI to improve clarity and readability. The historical context, reasoning, and overall flow are entirely my own.
I’ve been noticing something lately when I read about new infrastructure projects. Not the specific technologies, those change constantly, but the shape of the problems they’re solving. It’s like watching reruns of a show you half-remember: the set design is different, the actors have changed, but you know exactly how this episode goes.
The more time I spend in this industry, the more I see these patterns. Not because the people building new systems lack imagination, but because certain problems seem to be fundamental, built into the physics and economics of computation itself. We keep encountering them, solving them for our specific moment, and then watching them resurface a decade later in a different context.
Where Should the Logic Live?
Take the question of where computation should happen. In the 1970s, you had dumb terminals talking to smart mainframes. All the logic lived in one place, and the edge devices were just windows into that central brain. The reasons made sense: computers were expensive, so you pooled resources. Maintenance was easier when everything ran in one controlled environment. Users got consistent behavior because there was only one version of the truth.
Then personal computers flipped the script. Suddenly the intelligence moved to the desktop, and servers became glorified file cabinets. This also made sense: PCs were getting cheaper and more powerful, networks were still slow and unreliable, and users wanted software that worked even when disconnected. Fat clients meant responsive interfaces and no network lag for every interaction.
We’ve been doing this dance ever since.
The web brought us back to thin clients, with Netscape and Internet Explorer as the new dumb terminals. Why? Because deployment was easier when you didn’t have to install software on every machine. Because cross-platform compatibility mattered. Because automatic updates meant everyone was on the same version. The pendulum swung back toward centralization.
Then JavaScript frameworks made browsers smart again, turning them into application platforms that happened to run in a window. Single-page applications moved the logic back to the client because users expected instant interactions, and round-tripping to the server for every state change felt slow. React and Vue and Angular are fat clients by another name.
Now we’re watching the same pattern play out with edge computing. CDNs aren’t just caching static files anymore, they’re running actual application logic, closer to users, because latency matters and central data centers are too far away. Cloudflare Workers, Lambda@Edge, Deno Deploy. It’s the mainframe-terminal model again, except the “mainframes” are distributed globally, and the “terminals” are still smart enough to do their own processing.
Where Should Security Decisions Live?
The same tension shows up in authorization and access control, the question of who gets to do what in your system. This one’s particularly interesting because the stakes are higher. Get computation placement wrong and things are slow. Get authorization wrong and you have a security breach.
In early applications, authorization logic lived entirely in the code. You’d see it scattered throughout: if statements checking whether this user has this role before letting them access this resource. Simple systems, simple rules. Everything in one place where you could see it.
Then systems got bigger and more complex. Multiple applications needed to share user directories, so we centralized identity with LDAP and Active Directory. But the authorization logic, the actual permission checks, still lived in every application. Each codebase maintained its own rules about what different roles could do.
OAuth and OIDC helped with the authentication part. You could delegate “prove who you are” to a specialized system. But “what are you allowed to do” remained embedded in application code. Every service still had conditionals checking permissions, and if you wanted to change who could access what, you were modifying code and redeploying.
Now we’re seeing authorization logic get externalized again. Systems like Cerbos separate permission policies from application code entirely. Instead of scattering if statements throughout your codebase, you define authorization rules in a central policy layer that applications query at runtime. Your code asks “can this user do this action?” and gets back a decision based on policies that can be updated independently.
It’s the same fundamental question we’ve been asking about computation: should this logic live close to the data (embedded in the application) or separated into a specialized system you can manage independently?
And like the computation question, the answer keeps shifting based on constraints. When you have three developers working on a monolithic application, embedding authorization in code is straightforward. When you have twenty teams managing microservices and you need security officers to audit and modify access rules without code changes, externalized policies start making sense.
The trade-offs are real. Externalized authorization gives you flexibility, product managers can adjust permissions without waiting for engineering cycles. It gives you auditability, you can see all your access policies in one place rather than hunting through codebases. It gives you consistency, the same policy engine evaluating every request.
But you accept costs. Now you have another system to run and monitor. You’ve introduced network calls for authorization decisions. You need to think about what happens when the policy engine is unreachable. You’ve traded deployment complexity for operational complexity.
How Should Work Get Done?
Or look at how we think about scheduling and executing work. Mainframe computing was fundamentally batch-oriented. You submitted jobs to a queue, they ran when resources were available, and you picked up the results later. Interactive computing was expensive, so you batched up your work and waited.
As systems got more powerful and interactive computing became feasible, we moved away from that model. Real-time responses became the expectation. Cron jobs were the vestigial tail of batch processing, scheduled tasks you’d run at night because nobody wanted to admit we were still doing batch jobs.
Then data got big again, and suddenly batch processing was back, rebranded as MapReduce. Same fundamental idea: submit work to a scheduler, it runs across many machines when resources are available, you pick up the results. The technology was different, distributed systems across commodity hardware instead of time-sharing on a mainframe, but the shape of the solution was familiar.
Now we’ve got stream processing frameworks like Apache Flink and Kafka Streams, and there’s this interesting tension between batch and streaming that feels like a negotiation rather than a victory. Some problems really do need batch processing. Some need real-time streams. Most need both, depending on the question you’re asking.
What This Actually Means
What makes this interesting to me isn’t nostalgia for old systems or skepticism about new ones. It’s recognizing that certain problems are structural, they’re built into the fundamental constraints of computation, networks, human cognition, and economic forces. You can’t solve them once and for all. You can only solve them for your particular moment, with your particular constraints.
The mainframe folks knew about distributed processing. They had remote job entry systems and ways to balance load across multiple machines. The PC revolution people understood centralized coordination, that’s why client-server architecture became a thing. The web developers who built thin clients in the browser understood the trade-offs with fat clients.
We’re not smarter than the engineers from previous eras. We’re just working with different constraints, and those constraints keep revealing the same fundamental tensions.
I think about this when I see coverage of some new infrastructure approach. The excitement might be justified, the new solution might genuinely be better for right now. But “better for right now” is different from “finally solved.” The constraints will shift again. Scale will change, or compliance requirements will change, or team structures will change, and suddenly the old trade-offs are back on the table.
Maybe that’s actually useful to know. If you recognize the pattern, you can ask better questions. Not “is this new?” but “what constraints changed to make this trade-off viable again?” Not “will this solve the problem forever?” but “how long until the constraints shift and we need to rethink this?”
When you externalize authorization with something like Cerbos, you’re not discovering a new concept, you’re rediscovering that specialized, centralized policy management makes sense at certain scales and organizational structures. When you put logic at the edge with Cloudflare Workers, you’re not inventing distributed computing, you’re rediscovering that bringing computation closer to users matters when latency is your constraint.
The technologies we build aren’t just solutions. They’re arguments with physics and economics and human behavior, arguments that never quite end, just pause and resume in different contexts. The mainframe people and the PC people and the web people and the cloud people and whoever comes next aren’t wrong. They’re just having the same argument with different constraints.
What patterns from past computing eras are we rediscovering now, and what does that tell us about which problems are temporary and which ones are just… built into the shape of the world?