The Complexity Problem
Applications have been shipped in React that became unmaintainable, Vue apps where data flow was opaque, and Angular apps where implementing a simple feature took a week because side effects were impossible to trace.
Every project started simple—but complexity always found its way in.
The issue wasn’t the framework itself; it was that the frameworks allowed complexity to creep in silently. Nothing prevented adding another global store. Nothing warned when a component depended on seventeen different pieces of state. Nothing forced consideration of where data lived or how it flowed.
Svelte 5 is different—not because it’s “magical”, but because it is opinionated in ways that make simplicity the default path.
This series focuses on building systems that remain calm—not just at the beginning, but after six months, a year, or even when team members change.
What CALM Means
CALM is both an acronym and a promise:
- Contained: Every piece of state has a single, explicit owner.
- Automatic: Reactivity happens without manual wiring.
- Local: State lives as close as possible to where it is used.
- Minimal: Every dependency, abstraction, and feature must earn its place.
Together, these principles define a mental model that is easy to understand, prevents accidental complexity, enables confident feature modifications, and ensures long-term maintainability
Contained: Ownership You Can See
State belongs somewhere specific. Not “somewhere in the app.” Not “in a store that anything can access.” State should always have a clear home. Somewhere you can point to.
+---------------------+
| +page.server.ts |
| (authoritative) |
+----------+----------+
|
v data flows down
+---------------------+
| +page.svelte |
| $state (UI only) |
+----------+----------+
|
v props
+---------------------+
| Child Components |
| (read via props) |
+---------------------+
One owner, many readers, controlled mutation In Svelte 5 with SvelteKit, state is contained by default:
- Route state lives in load functions and page components
- Component state lives in the component that needs it
- Global state is rare and exists only when explicitly required
<!-- Component with contained state -->
<script>
let isOpen = $state(false)
</script>
<button onclick={() => (isOpen = !isOpen)}> Toggle </button>
{#if isOpen}
<div class="dropdown">...</div>
{/if} Ownership is now explicit and traceable. Removing a component removes its state. New developers can understand it by looking at one folder.
When you look at a component, you should be able to answer: “Where does this data come from?” If the answer is “I’d have to check five different files,” something went wrong.
Contained state means:
- You can understand a component by reading it
- You can delete a route without breaking distant parts of the app
- You can onboard a new developer by showing them one folder
Automatic: Reactivity Without Effort
Reactivity should be automatic. You shouldn’t have to remember to call setState. You shouldn’t have to wrap things in useMemo to avoid re-renders. You shouldn’t have to think about dependency arrays.
$state $derived UI
+--------------+ +--------------+ +--------------+
| count = 0 | > | doubled = 0 | > | renders "0" |
+--------------+ +--------------+ +--------------+
|
| count++
v
+--------------+ +--------------+ +--------------+
| count = 1 | > | doubled = 2 | > | renders "2" |
+--------------+ +--------------+ +--------------+
No dependency arrays. No manual subscriptions. It just works. Svelte 5’s runes make reactivity automatic:
<script>
let count = $state(0)
let doubled = $derived(count * 2)
</script>
<button onclick={() => count++}>
{count} × 2 = {doubled}
</button> You change count. The UI updates. You don’t think about it.
This isn’t laziness—it’s correctness. Manual reactivity is a source of bugs. Forgotten dependencies. Stale closures. Unnecessary re-renders. Svelte eliminates the category.
Automatic reactivity means:
- Less code to write
- Fewer bugs to fix
- More time to think about your actual problem
Local: Keep State Where It Belongs
Think locally. Act locally. State should live as close as possible to where it’s used.
X Premature lifting OK Local-first
+-------------------+ +-------------------+
| App | | App |
| isOpen = $state | | |
+---------+---------+ +---------+---------+
| |
+-----+-----+ +-----+-----+
| | | |
v v v v
+-------+ +-------+ +-------+ +-------+
| Nav | | Main | | Nav | | Main |
+-------+ +---+---+ +-------+ +---+---+
| |
v v
+-----------+ +------------------+
| Dropdown | | Dropdown |
| (needs | | isOpen = $state |
| isOpen) | | (owns it!) |
+-----------+ +------------------+
State lives where it's used. Lift only when proven necessary. The instinct to “lift state up” or “put it in a store” is often premature. Most state doesn’t need to be shared. Most state is used by one component, or one route, or one small tree of components.
Local-first thinking means:
- Start with
$statein the component that needs it - Only lift when you actually need to share
- Only go global when you’ve proven local doesn’t work
<!-- This is usually enough -->
<script>
let isOpen = $state(false)
</script>
<button onclick={() => (isOpen = !isOpen)}> Toggle </button>
{#if isOpen}
<div class="dropdown">...</div>
{/if} No store. No context. No prop drilling. Just state where it’s needed.
Global state isn’t evil—it’s just expensive. Every piece of global state is a dependency that any component might have. It’s a thing everyone on the team needs to understand. It’s a coupling point that makes refactoring harder.
Local-first means paying that cost only when you get real value in return.
Minimal: Every Dependency Must Earn Its Place
The best code is the code you didn’t write. The best abstraction is the one you didn’t create.
+------------------------------------------+
| Need state management? |
+---------------------+--------------------+
|
+------------+------------+
| |
v v
+-----------------+ +-----------------+
| One component? | | Multiple routes?|
+--------+--------+ +--------+--------+
| |
v v
+-----------------+ +-----------------+
| Use $state | | Use load |
| (no library) | | functions |
+-----------------+ +-----------------+
Every abstraction must earn its place. Start simple. Minimal means:
- Don’t add a library until you’ve felt the pain it solves
- Don’t create an abstraction until you’ve written the same code three times
- Don’t optimize until you’ve measured
Svelte helps here too. Because reactivity is built in, you don’t need state management libraries. Because the compiler is smart, you don’t need memoization wrappers. Because the syntax is close to HTML, you don’t need component libraries for basic things.
Minimal doesn’t mean “never use libraries.” It means every dependency should earn its place. Every abstraction should justify its existence.
<!-- You probably don't need a Modal component library -->
<dialog bind:this={dialog}>
<h2>Are you sure?</h2>
<button onclick={() => dialog.close()}>Cancel</button>
<button onclick={confirm}>Confirm</button>
</dialog> The Promise
This series will teach you to build CALM systems with Svelte 5. By the end, you’ll have:
A mental model — You’ll understand why certain patterns work and others don’t. Not just “how to use $state” but “when state should exist and where it should live.”
A set of patterns — Concrete, reusable approaches for common problems. Data loading. Form handling. Error boundaries. Component communication. Patterns that scale.
Confidence — The knowledge that you’re not missing something important. That you don’t need to learn twelve more advanced techniques. That what you know is enough.
We’re not covering every feature. We’re covering the features that matter for building calm systems. The rest you can learn when—and if—you need them.
What We Won’t Do
We won’t:
- Chase every new feature — Experimental APIs are exciting, but calm systems are built on stable foundations
- Over-abstract prematurely — We’ll write “boring” code that’s easy to understand
- Optimize before measuring — We’ll trust the compiler until we have evidence we shouldn’t
- Add complexity to feel productive — We’ll resist the urge to architect when we should be building
The Path
Here’s what’s coming:
Foundation — The runes you actually need (hint: it’s six)
Mental Models — How to think about data flow, mutation, derived state, and side effects
Building — Practical SvelteKit patterns for routing, loading data, handling forms, and communicating between components
Maintenance — Patterns that keep your app simple as it grows, including the mistakes to avoid
Capstone — A complete application that demonstrates everything, so you can see how the pieces fit together
Each post builds on the last. Each pattern earns its place. By the end, you’ll have everything you need.
Who This Is For
This series is for developers who:
- Have been burned by complexity and want something simpler
- Are new to Svelte and want to start with good habits
- Know Svelte 4 and want to understand the Svelte 5 way
- Are tired of tutorials that show toys instead of real patterns
It’s not for developers who:
- Want to master every corner of the framework
- Need to know about experimental features right now
- Prefer comprehensive reference docs to opinionated guides
Let’s Begin
Calm isn’t about working slower. It’s about building systems that don’t fight you. Systems where the code you wrote six months ago still makes sense. Systems where new features go in the obvious place.
Svelte 5 makes this possible. This series will show you how.
Next up: the six runes you actually need. No more, no less.
Welcome to CALM with Svelte 5.