Clean Architecture protects the happy zone

bardiainjast1 pts0 comments

Clean Architecture protects the happy zone | Christian Rackerseder

Clean Architecture protects the happy zone<br>Christian RackersederBy Christian Rackerseder<br>Published 2026-06-27in Architecture12 min read

Permalink Clean Architecture is one of those ideas that is easy to draw and hard to keep in real code.

The onion diagram is useful because it shows one important rule: source code dependencies point inward. Outer code may know inner code. Inner code must not know outer code.

That sounds simple until you look at a frontend application and notice how often this rule gets violated quietly.

A use case receives a translation function. Business logic reads new Date() directly. Application code knows about a modal. A component passes raw network data deeper into the system because TypeScript was told to trust it. Storage, browser APIs, current time, user input and third-party responses slowly move closer to the center.

None of these things look dramatic in isolation.

Together, they make the important code expensive to change.

That is the real point of the onion.

Not folders.

Boundaries.

The onion is useful, but it is not the architecture #

The onion matters, but not because circles are architecture.

It matters because it shows what should be stable. The further inward code lives, the less it should know about the outside world. The center should not know whether the application is rendered with React, whether data came from HTTP, whether a message is shown in a toast, or whether the current language is English or German.

The inner part should know the rules.

The outer part should know the world.

When that direction is respected, the application becomes easier to reason about. When it is ignored, every small change starts to touch too many places. A UI decision becomes an application decision. A browser detail becomes a business rule. A response shape from one API becomes a type that leaks everywhere.

That is not Clean Architecture.

That is coupling with nicer folder names.

The outside world is evil #

I do not mean evil in a dramatic way.

I mean evil in a practical way.

The outside world is everything you do not fully control: user input, network responses, browser APIs, storage, date and time, random values, environment variables, third-party libraries, rendering and framework behavior.

All of these things are allowed to be messy. They are allowed to fail. They are allowed to change shape. They are allowed to be unavailable. They are allowed to be different tomorrow.

The mistake is not that the outside world is messy.

The mistake is letting that mess walk straight into the center of the application.

Dirty code at the edge is normal.

Dirty code in the center is expensive.

The DMZ exists for a reason #

Between the outside world and the happy zone, there should be a boundary.

Call it a boundary.

Call it an adapter layer.

Call it a DMZ.

The name is not important. The job is important: dirty data should not enter the application and pretend to be trusted.

This is where runtime validation belongs. This is where date parsing belongs. This is where user input verification belongs. This is where network response mapping belongs. This is where unknown failures become semantic failures.

The outside world gives you unknown.

The boundary turns it into application data.

This is bad:

import { isNumber, isPlainObject, isString } from "@sindresorhus/is";

type Order = {<br>id: string;<br>totalInCents: number;<br>};

async function loadOrderTotal(): Promisenumber> {<br>const response = await fetch("/api/current-order");<br>const order = (await response.json()) as Order;

return order.totalInCents;<br>The type assertion does not validate anything. It only tells TypeScript to stop asking questions.

The code looks typed, but the application is still trusting the outside world.

A boundary should be more explicit:

type Order = {<br>id: string;<br>totalInCents: number;<br>};

type ParseOrderResponseResult =<br>| { status: "valid"; order: Order }<br>| { status: "invalid"; reason: "invalidOrderResponse" };

function isRecord(value: unknown): value is Recordstring, unknown> {<br>return isPlainObject(value);

function parseOrderResponse(externalOrder: unknown): ParseOrderResponseResult {<br>if (!isRecord(externalOrder)) {<br>return { status: "invalid", reason: "invalidOrderResponse" };

const id = externalOrder["id"];<br>const totalInCents = externalOrder["totalInCents"];

if (!isString(id)) {<br>return { status: "invalid", reason: "invalidOrderResponse" };

if (!isNumber(totalInCents)) {<br>return { status: "invalid", reason: "invalidOrderResponse" };

return { status: "valid", order: { id, totalInCents } };<br>This is not an argument for handwritten parsers. In real code, I would often use Zod, ArkType, Valibot or a small parser here. The tool is not the architecture.

The boundary is the architecture.

The important part is that the happy zone receives an Order, not a random JSON blob with a TypeScript costume.

This is also why I care about avoiding...

code order architecture application world outside

Related Articles