Four decisions that determine what the rest of a codebase is allowed to look like — in any language, which is the point. They are cheap on day one and expensive to retrofit in year three, and that asymmetry is most of the reason to have opinions about them at all.
Errors as values
railway-oriented programming, after Scott Wlaschin
Expected failure is part of the return type, not an exception thrown past you.
Services return a result type rather than throwing for a state the caller should handle, and the shape reaches all the way down: the database layer translates constraint violations into typed errors instead of exceptions, and one place at the edge maps the error union onto status codes.
In other stacksRust ships it in the standard library, which is where most people meet it. Kotlin expresses it with a sealed hierarchy, C# with a result type or a discriminated union, Go with the error return it has always had, TypeScript with a tagged union. Only the spelling changes.
What it costsYou write mapping code a framework-native codebase gets for free. And transactions are the deliberate exception: inside one, a rollback still throws.
Functional core, imperative shell
Gary Bernhardt; ports and adapters, after Alistair Cockburn
The domain takes values and returns values. Everything that talks to the world stays at the edge.
Domain packages contain zero framework imports. When the APIs moved from one HTTP framework to another, the migration was a week of route files and not one line of domain logic. The same boundary is why the payment providers could be removed wholesale without changing settlement semantics.
In other stacksIt is the same rule Clean Architecture states for .NET, where the domain project references no web framework and the handlers sit outside it, and the same one hexagonal architecture states for the JVM. A dependency rule pointing inwards is not a language feature.
What it costsYou give up the ergonomic parts of modern frameworks — decorators that inject services into handlers, plugins that stash state on the request — or confine them to the edge where they cannot reach the domain.
Make illegal states unrepresentable
Yaron Minsky
The dangerous call should be a sentence you cannot write, not a bug you have to catch.
Whatever scopes a request — the tenant, the account, the authenticated actor — is a construction dependency, resolved once at the edge and closed over, never a parameter each method restates. There is then no argument through which the wrong scope can be passed, so the review question "did we remember the filter" stops existing.
In other stacksThe tools differ and the move does not: sealed interfaces and exhaustive `when` in Kotlin, enums with payloads and an unrepresentable invalid variant in Rust, records plus a closed hierarchy in modern C#, discriminated unions in TypeScript. Where a type system cannot express it, a constructor that refuses invalid input gets most of the way.
Parse, don’t validate
Alexis King
Untrusted input becomes a typed value once, at the boundary, or it does not get in.
Schemas at every entry point, and configuration behind a single validated door: the application refuses to start rather than discovering a missing variable at 3am under load. Derived schemas keep the second definition from existing at all — add a config key in one place and the API accepts it.
In other stacksZod in TypeScript, serde with typed structs in Rust, model binding plus validation attributes in .NET, kotlinx.serialization on the JVM. The principle is that the parsed value has a different type from the raw one, which every one of those can express.