The Conservation Principle
How Software Moves Complexity Instead of Removing It

The Simple Button
ElevatedButton(
onPressed: save,
child: const Text('Save'),
)
A tiny declaration. One tap target. One word on it.
If I asked you how much work "draw a button" represents, you'd probably shrug. Not much, right? It's a button. We've all pressed a thousand of them today without thinking twice.
But hold that question for a second - how much work does this actually represent - because by the end of this article, I want it to stop feeling like a trivial question. I want it to feel like the only question that matters.
In my last article, I wrote about how a high-level concept like Greeting(person: Advay) dissolves, step by step, into the primitives Flutter actually understands - until eventually it becomes pixels on a screen. Dissolution was about transformation: how something complex becomes something simple enough for a framework to execute.
But dissolving something isn't the same as destroying it. And that distinction is where this article lives.
If Dissolution asks "how does complexity dissolve?", this article asks the question that's been waiting underneath it the whole time:
Where did the complexity go?
The Hidden Complexity
Let's actually zoom in on that button.
ElevatedButton is a Dart class. When Flutter builds it, it doesn't stay a button for very long - it expands into a Material widget, wrapped in an InkWell for the ripple effect, wrapped in constraints and padding, wrapped in a GestureDetector that's listening for your finger. This is a widget tree, and it's already several layers deep before you've done anything.
That widget tree isn't what actually gets drawn, though. Flutter takes it and builds a parallel structure called the element tree - persistent objects that know how to update themselves efficiently instead of being thrown away and rebuilt every frame. Each element is tied to a render object, and the render objects are the ones that actually do the geometric work: figuring out how big the button should be, where it sits relative to its siblings, how it should be painted.
Painting means turning those render objects into drawing commands - rectangles, text runs, shadows, clip paths - which get handed to Skia (or, in newer Flutter builds, Impeller), Flutter's graphics engine. Skia turns those commands into GPU instructions. The GPU turns those instructions into a grid of colored pixels. The compositor hands that grid to your operating system's display pipeline, which hands it to the actual physical panel in front of your eyes, which changes the voltage across a few hundred thousand tiny electrodes so that a rectangle appears to glow the right shade of blue.
ElevatedButton
↓
Widget tree (Material, InkWell, GestureDetector...)
↓
Element tree
↓
Render objects
↓
Layout (size & position)
↓
Painting (drawing commands)
↓
Skia / Impeller (GPU instructions)
↓
Pixels
Somewhere in the middle of all that, your finger touched glass, and an operating system had to figure out which of the thousands of things on screen you meant to touch.
You wrote four lines. The system performed an enormous amount of work to make good on those four lines.
Wait. All of that was hiding underneath this one line?
Yes. And here's the thing worth sitting with: none of that work was optional. It didn't happen because Flutter is inefficient or over-engineered. It happened because drawing a button on a screen is, underneath any framework's syntax, a genuinely complicated act. You just didn't have to think about it.
The Complexity Transfer
This is the part where once you see it, you start seeing it everywhere.
When you wrote ElevatedButton(...), you weren't given something simpler than the layout-painting-GPU pipeline. You were given something smaller to think about. The complexity of the pipeline didn't shrink. It moved - out of your file, and into Flutter's.
Your application
↓
Simple API ← you deal with almost nothing
↓
Framework absorbs complexity ← Flutter deals with widget→render translation
↓
Runtime absorbs complexity ← Dart VM / AOT compiled code deals with execution
↓
Operating system absorbs complexity ← deals with compositing, input, memory
↓
Hardware absorbs complexity ← deals with the actual electrons
Each layer earns its keep by taking on responsibility that the layer above no longer has to carry. That's not a side effect of abstraction. It is abstraction.
The Conservation Principle: Complexity is not eliminated by abstraction. It is relocated - from the layer that wants to be simple, into the layer that agrees to become more complex on its behalf.
I want to be careful here. This isn't a physical law, and I'm not claiming software obeys some literal thermodynamics. I'm not going to try to prove that complexity is numerically invariant across every transformation, because I don't think that's a claim you can actually justify. What I can justify, and what I think is worth calling a principle, is the pattern: every time you look closely at a place where complexity seems to have vanished, you find it again one layer down, wearing a different shape.
Conservation, here, is a lens - not a theorem.
Complexity Has Different Forms
Here's where the principle needs to get more precise, because "complexity" is doing a lot of unexamined work in that sentence above. If I just said "complexity moves down a layer," you could reasonably ask: complexity of what, exactly?
It helps to separate out a few distinct kinds:
Structural complexity - how many components exist, and how they relate to each other.
Cognitive complexity - how much a person has to hold in their head to reason about the system correctly.
Temporal complexity - how the system behaves differently depending on when something happens, across state changes and lifecycles.
Operational complexity - what it takes to deploy, monitor, and keep the thing running.
Dependency complexity - how many external systems and libraries the system now quietly relies on.
Conceptual complexity - how many distinct ideas a person needs to understand before any of it makes sense.
This distinction matters because Conservation doesn't say complexity moves unchanged. It usually moves in a different form entirely. Here's the trade the button actually makes:
ElevatedButton(...)
↓
Lower cognitive complexity (you don't think about render objects)
↓
Higher dependency complexity (you now depend on Flutter's rendering pipeline being correct)
You didn't just move the same kind of complexity somewhere else. You converted it - you traded cognitive load for dependency risk. That's a much more interesting exchange than "it got harder somewhere else," and it's the exchange that most abstraction decisions are actually making, whether or not anyone says so out loud.
The Abstraction Trade
This is close to the center of the whole article, so let me try to say it as cleanly as I can:
Abstraction is not the removal of complexity. It is the assignment of complexity.
When you choose ElevatedButton over hand-rolled canvas painting, you are not choosing "less complexity." You are choosing who has to deal with it - and, implicitly, choosing to trust the Flutter team's implementation more than you trust your own.
When a team chooses an ORM over raw SQL:
User.objects.filter(active=True)
they aren't eliminating the complexity of query planning, index selection, or connection pooling. They're assigning it to the ORM's authors, and accepting, in exchange, a slightly less precise mental model of what SQL will actually run.
When a company chooses cloud infrastructure over physical servers, they aren't removing the complexity of provisioning, cooling, or hardware failure. They're assigning it to a cloud provider's operations teams, in exchange for a simpler deploy command.
Neither trade is free, and neither trade is a mistake. That's the part worth resisting the urge to moralize about. Abstraction earns its place because most of us, most of the time, genuinely don't need to deal with complexity that's irrelevant to what we're trying to build. Assigning it elsewhere is often the correct engineering decision. The mistake isn't making the trade - it's forgetting that you made one.
Beyond Flutter
Some other examples of the abstraction trade:
ORMs. User.objects.filter(active=True) instead of writing SQL by hand. Query complexity didn't disappear - it moved into the ORM's query builder, and a new kind of complexity appeared in its place: knowing which method calls compile down to efficient SQL and which ones silently generate a query that will bring your database to its knees.
Cloud computing. deploy instead of racking a server. Infrastructure complexity moved into the provider's data centers - and a new form appeared on your side: billing complexity, IAM permission complexity, the complexity of debugging a system you can no longer physically walk over to and look at.
UI frameworks. Writing Column(children: [...]) instead of manually blitting pixels. Rendering complexity moved into the framework - and widget-tree complexity appeared in its place: the discipline required to keep a deeply nested tree of widgets legible, and the debugging skill required to read a stack trace that runs three layers deeper than the code you wrote.
High-level programming languages. Writing x + y instead of managing registers by hand. Machine-level complexity moved into the compiler - and a new complexity appeared: understanding what your abstractions actually cost, since the language will happily let you write something elegant and slow in the same breath.
AI coding assistants. Writing one sentence and getting back two hundred lines of working code. The complexity of producing the code moved almost entirely onto the model - and a new complexity has appeared to take its place: the complexity of understanding and governing code you didn't write line-by-line. You paid less to generate it. You may now pay more to verify it, maintain it, or explain why it's correct.
That last example might be the cleanest modern instance of the whole principle. Nothing about complexity got smaller. The bill just moved from the "writing" column to the "understanding" column - and those are very different currencies to pay in.
The Complexity Budget
Here's a way to hold all of this at once, as a mental model rather than a formula:
Complexity Budget
=
what the developer understands
+
what the framework understands
+
what the runtime understands
+
what infrastructure understands
+
what the system simply hides and hopes never breaks
I don't think this equation balances in any way you could put numbers to. I'm not claiming these quantities are commensurable, or that the sum is conserved exactly. What I am claiming is that every system has some total amount of "things that have to be dealt with correctly for it to work," and that total doesn't shrink just because you've stopped looking at most of it.
The useful question this framing gives you isn't "how do I reduce total complexity" - that's often not on the table. It's:
Who is paying the complexity bill, and can they actually afford it?
Sometimes the answer is yes, easily - that's most of what makes good abstractions good. Sometimes the answer is "the on-call engineer, at 3 a.m., six months from now," and that's usually where things go wrong.
When Abstraction Becomes Dangerous
Every abstraction is a bet: that the complexity it hides will cost less to leave hidden than it would cost to understand.
An abstraction is successful when the complexity it hides is less costly to ignore than it would be to understand.
Most of the time, this bet pays off beautifully. You don't need to understand TCP retransmission to make an HTTP request. You don't need to understand garbage collector internals to write a loop.
But the bet can go bad in fairly specific, recognizable ways: when the abstraction leaks and starts requiring you to know what's underneath it anyway; when "magic" behavior means you can't predict what your own code will do without reading the framework's source; when a dependency chain grows so long that a single upgrade breaks things three layers away from where you touched anything; when a system is so configuration-heavy that using it correctly requires almost as much knowledge as building it would have.
In each of these cases, the complexity didn't get more expensive to deal with. It got more expensive to ignore - and that's the moment the bet stops paying off.
The Return of Complexity
Here's where Conservation folds back into Dissolution, and where I think the two articles are really talking about the same event from two different sides.
Normally, your relationship to the stack looks like this:
Developer
↓
Framework
↓
Runtime
↓
Hardware
You stay at the top. You trust the layers below you to have already dealt with their share of the complexity budget, and you go about your day.
But when something breaks - a frame drops, a state update doesn't propagate, a widget rebuilds when it shouldn't - the boundary you were relying on stops protecting you:
Developer
↓
Framework internals
↓
Runtime internals
↓
OS behavior
↓
Hardware behavior
You're no longer using the abstraction. You're forced to dissolve it - to trace your four-line widget back down through the widget tree, the element tree, the render objects, the paint calls, until you find the layer where the promise broke down.
This is the relationship between the two principles, stated as plainly as I can:
Dissolution is what happens when you descend through an abstraction. Conservation is what explains why there was still complexity waiting for you when you got there.
The abstraction never actually removed anything. It just moved the complexity somewhere you weren't looking - and debugging is the act of looking there anyway.
Conclusion
Go back to the button.
ElevatedButton(
onPressed: save,
child: const Text('Save'),
)
Or even smaller - just the text inside it:
Text('Hello, Advay')
Eleven characters of intent. Underneath them: widget construction, element diffing, layout constraints, paint commands, GPU shaders, a font rasterizer deciding the exact curve of every letter, a display panel changing voltage across a grid of electrodes, faster than you can perceive it happening.
That is not a flaw in the system. It is the entire point of the system. You don't need to understand a font rasterizer to say hello to someone on a screen. You only need the right abstraction, sitting at the right layer, having already made its trade on your behalf.
The goal of software engineering was never to eliminate complexity. There may be no such thing as eliminating it - only deciding, deliberately or by accident, where it should live, and who should be the one to answer for it when it resurfaces.
So here's the question I actually want you to leave with, the one I think is worth carrying into every framework, every library, every "it just works" you encounter from now on:
If this got simpler here - where did it go?
Dissolution shows you how a concept breaks apart into what a machine can execute. Conservation reminds you that nothing in that breakdown was ever free - only moved, until something, somewhere, has to hold it.

