The Bedrock of Software Design | Alex FedoseevThe Bedrock of Software Design<br>2026, 31 Jul·
Nothing has influenced the way I design software more than algebraic data types .<br>It sounds overly academic—the kind of term that makes people roll their eyes or scares them away altogether. But honestly, it only sounds scary. The idea itself is quite simple, and learning it showed me a fundamentally better way to build software.<br>A few years ago, I even started recording a video series about ADTs. I finished one and a half episodes; then life happened. Consider this post the written and compressed version I should have finished years ago.<br>Theory<br>I hate doing this part. Theory is boring most of the time, and I would rather jump straight to the good stuff. You can skip this section entirely, to be honest. But I need to lay down a couple of terms so they do not make you go “WTF?” later. I’ll keep it short and on-point.<br>There are two fundamental kinds of algebraic data types.<br>Product types<br>Product types are roughly what objects look like in mainstream languages—roughly, because objects often come with extra baggage.<br>rust
struct User {<br>id: UserId,<br>role: Role,<br>}A User has an id and a role. Every User contains both—that is what makes it a product type.<br>Sum types<br>The king!<br>A sum type says that a value is one of several possible variants:<br>rust
enum Session {<br>Anonymous,<br>Authenticated { user: User },<br>}A Session is either Anonymous or Authenticated. Better yet, every variant can carry its own data: a User exists only when the session is Authenticated.<br>Most of the benefits in this post come from this one deceptively simple idea.
Why “product” and “sum”?
Okay. Theory over. Let’s get to the good stuff: why these things are actually useful.<br>Let types enforce the rules<br>Every non-trivial domain is full of rules. Some values must exist together. Others must never exist at the same time. Sometimes which fields are required depends on the value of another field.<br>Too often, these rules live only in code comments, documentation, validation functions, or developers’ heads. That is fragile as hell, because documentation gets outdated, validation gets skipped, and people forget.<br>A well-designed sum type encodes rules into the type itself. Anyone reading it can see which values can exist and how they fit together, while the compiler rejects everything else. Often, the type is all the documentation you need.<br>To make it clear what I mean, let’s take the color settings for a website. A site can either let visitors switch between light and dark modes or use one fixed mode.<br>A switchable site needs a default mode and color schemes for both modes. A fixed site needs one mode and one scheme.<br>We could model that with a boolean and a pile of nullable fields:<br>ts
type SiteTheme = {<br>switchable: boolean;<br>defaultMode: ColorMode | null;<br>lightScheme: ColorScheme | null;<br>darkScheme: ColorScheme | null;<br>fixedMode: ColorMode | null;<br>fixedScheme: ColorScheme | null;<br>};This type allows many nonsensical configurations. A switchable theme can be missing one of its schemes. A fixed theme can have no fixed scheme. Both configurations can be present at the same time, or every nullable field can be null. The type accepts all of it.<br>To know which combinations are actually valid, you need documentation, validation, or tribal knowledge. Every part of the program that receives this value must either trust that someone checked it earlier or defend itself against complete nonsense.<br>Now let’s model what the domain actually says:<br>rust
enum SiteTheme {<br>Switchable {<br>default_mode: ColorMode,<br>light_scheme: ColorScheme,<br>dark_scheme: ColorScheme,<br>},<br>Fixed {<br>mode: ColorMode,<br>scheme: ColorScheme,<br>},
enum ColorMode {<br>Light,<br>Dark,<br>}The type describes exactly two possibilities.<br>A Switchable theme always has a default mode and both schemes. A Fixed theme always has one mode and one scheme. Required values can’t be omitted, and fields from one configuration can’t leak into the other. Every SiteTheme value satisfies these rules. Guaranteed.
Another example: component APIs
This is what “make invalid states unrepresentable” means: instead of worrying about nonsense, design the type so it cannot exist in the first place.<br>Make errors part of the contract<br>A function’s signature should describe not only what it returns when it succeeds, but also whether it can fail.<br>In languages built around exceptions, the return type usually describes only the happy path:<br>ts
function parsePort(value: string): number {<br>const port = Number(value);
if (!Number.isInteger(port) || port 65535) {<br>throw new Error("Invalid port");
return port;<br>}From the function signature alone, we see only that parsePort returns a number. To discover that it can fail, we have to read its documentation or, better yet, inspect its implementation. We may also need to inspect every function it calls, since any of them might throw an exception of its own.<br>Honestly, I go into paranoid mode whenever I work in an exception-based...