How Complexity Creeps In
Nobody sets out to build a complicated codebase.
It starts simple. A few components. Some shared state. A utility function or two. You can hold the whole thing in your head.
Then it grows.
Someone adds a global store because passing props felt tedious. Someone else creates a hook that subscribes to three different stores because the data was needed in multiple places. A third developer abstracts the hook into a higher-order component because the subscription logic was duplicated.
Each decision made sense in isolation. Each one was “clever”—it solved an immediate problem with an elegant abstraction.
Six months later, nobody understands how data flows through the app. Changing a button’s behavior requires tracing through four files. New developers take weeks to become productive. The simple app isn’t simple anymore.
This is the cost of cleverness.
Clever Feels Productive
Clever code has a seductive quality. It feels like progress. You’re not just solving a problem—you’re solving a category of problems. You’re building infrastructure. You’re thinking ahead.
The abstraction that handles five edge cases. The factory function that generates components. The state machine that manages all possible UI states. These feel like investments.
But abstractions have carrying costs:
Abstractions must be understood. Every developer who touches the code needs to learn what the abstraction does, how it works, and when to use it. This takes time—time that compounds with every new team member.
Abstractions must be maintained. When requirements change, the abstraction must change too. But abstractions are harder to change than plain code because they serve multiple use cases. Change one, risk breaking others.
Abstractions hide complexity. They don’t eliminate it. The complexity still exists—it’s just been moved somewhere less visible. Out of sight isn’t out of mind when you’re debugging at 2 AM.
The clever solution that saved you an afternoon costs the team weeks over the lifetime of the project.
Implicit Dependencies Are the Root of Most Bugs
Here’s a pattern that has destroyed countless codebases:
Component A updates a global store. Component B reads from that store and computes a derived value. Component C reads the derived value and makes an API call. Component D subscribes to the API response and updates a different store. Component A reads from that store.
Congratulations, you’ve built a circular dependency graph that nobody can visualize without a whiteboard.
The problem isn’t global state itself. The problem is implicit dependencies—relationships between pieces of code that aren’t visible where the code is written.
When you read Component A, you don’t see that it triggers a cascade ending back at itself. When Component C stops working, you don’t know to look at Component A. When you want to delete Component B, you have no idea what breaks.
Implicit dependencies create:
Spooky action at a distance. Changing code here breaks code there, with no visible connection.
Impossible-to-predict side effects. You can’t know what a function does without tracing its entire dependency graph.
Fragile tests. Unit tests pass, integration tests fail, because the units don’t know about each other.
The more implicit dependencies in your code, the more your codebase behaves like a house of cards.
Frameworks Can Enable or Prevent This
Some frameworks make it easy to create implicit dependencies. Some make it hard.
React’s useEffect with a dependency array is a source of implicit coupling. The effect depends on values, but the connection is just an array of variables that the developer must maintain. Forget one, and you have a bug that only appears under certain conditions.
Redux’s connect function creates implicit coupling between components and the store shape. Rename a property in the store, and you’ll find out which components depended on it at runtime.
Vue’s mixins merge behavior in ways that are invisible at the point of use. Two mixins can conflict in ways you won’t discover until they’re both included.
These aren’t bad frameworks. They’re powerful tools that trust developers to be disciplined. The problem is that discipline fails at scale. The more developers, the more time pressure, the more context-switching—the more implicit dependencies sneak in.
Svelte 5 takes a different approach.
Svelte Makes Simplicity the Default
Svelte 5’s runes make dependencies explicit by design.
When you write $derived(count * 2), the dependency on count is visible right there. Not in a separate array. Not in a subscription call. Not inferred by a framework at runtime. Right there.
When you write $effect(() => { ... }), Svelte tracks what you read automatically. You can’t forget a dependency because you don’t declare them—Svelte sees them.
When you want shared state, you can’t accidentally create implicit coupling. State declared with $state in a module is explicitly imported where it’s used. The dependency is visible in the import statement.
// lib/counter.svelte.js
export const count = $state(0);
// Component.svelte
import { count } from '$lib/counter.svelte.js';
// The dependency is explicit: this component uses count This doesn’t prevent all complexity. You can still build tangled systems in Svelte. But the framework doesn’t help you do it. The path of least resistance leads toward explicit, traceable dependencies.
The Choice
Every time you write code, you’re choosing between:
Clever: Solve the general case. Build an abstraction. Handle all the edge cases now so you won’t have to later.
Simple: Solve the specific case. Write plain code. Handle edge cases when they actually appear.
Clever feels responsible. Simple feels naive.
But simple code is:
- Easier to read (no abstraction to learn)
- Easier to change (no other use cases to consider)
- Easier to delete (no dependents to update)
- Easier to debug (no layers to trace through)
Simple code becomes clever code when needed. Clever code rarely becomes simple again.
The best developers share one thing in common: they’re suspicious of their own cleverness. They ask “do we need this abstraction?” before building it. They wait for the third use case before generalizing. They prefer boring, obvious code over elegant, clever code.
What This Means for You
As we go through this series, you’ll notice we don’t build many abstractions. We write components that do one thing. We keep state close to where it’s used. We let the framework do the clever parts so our code can stay simple.
This isn’t because abstraction is bad. It’s because abstraction has a cost, and we want to pay that cost only when we get real value in return.
When you finish this series, your Svelte code will probably look boring. That’s the goal. Boring code is calm code. Calm code is code that works six months from now.
Complexity isn’t inevitable. It’s a choice we make, one clever abstraction at a time.
Choose simplicity instead.
Next up: We’ll shift from “why” to “how”—specifically, how thinking in data flow (instead of lifecycle) prevents entire categories of bugs.