Ever heard folks rave about ADTs and wondered how that maps to plain old Java? Let’s demystify it—and put it to work with record, sealed, and pattern matching. 💪

What is an Algebraic Data Type? 🧠
An ADT is a way to model data precisely using two building blocks:
1) Product type (AND) ✖️ A type composed of multiple fields—think “A and B”. Mathematically, the number of possible values multiplies.
- Examples: a 2D point (x AND y), a money amount (value AND currency).
- In Java: a class or (better) a record.
2) Sum type (OR) ➕ A type that is one of several alternatives—think “A or B”. Mathematically, possibilities add up.
- Examples: a payment that can be Cash OR Card OR GiftCard.
- In Java: a sealed interface + record implementations (a tagged union). (Enums are a simpler sum type—great when each case has no or fixed payload.)
Explanation from Reddit
"Algebraic" just means you have both product types (e.g. tuples, structs, or pairs) and sum types (discriminated unions), like Either a b = Left a | Right b. Why the name? Forget everything about a type except how many possible different values it can have. So e.g. bool = 2, int8 = 256, () = 1, etc. Then a tuple (bool, uint8) has 2 * 256 = 512 possible combinations, and in general (a, b, c) = a * b * c -- product types! Likewise Option bool has 3 possibilities (None, Some true, Some false), and generally Option t = t + 1, Either a b = Left a | Right b = a + b. Sums! The connection to algebra is actually fairly deep; functions a -> b correspond to powers b^a, which is the same as a tuple (a1, a2, a3, ...a_b), i.e. a lookup table. You can manipulate types algebraically, figure out what subtraction and division and even derivatives correspond to and it's all kinda neat. There are other ways you can go with a type system, though. E.g. union types like in Typescript are not sum types, but they're still very useful.
ADTs in Java: Idiomatic Patterns (Java 17–21+) ☕️
Product type → record
public record Money(long cents, String currency) { public Money { if (cents < 0) throw new IllegalArgumentException("negative"); if (currency == null || currency.isBlank()) throw new IllegalArgumentException("currency"); } }
- Reads like data, immutable by default, compact constructor = validations/normalization.
Sum type → sealed hierarchy + record cases
public sealed interface Payment permits Cash, Card, GiftCard {} public record Cash(long cents) implements Payment {} public record Card(String maskedPan, long cents) implements Payment {} public record GiftCard(String code, long cents) implements Payment {}
Handle all variants with pattern matching for switch (Java 21, JEP 441):
public long fee(Payment p) { return switch (p) { case Cash c -> 0; case Card c -> Math.round(c.cents() * 0.02); case GiftCard g -> 0; }; // exhaustiveness checked by the compiler ✅ }
A classic Sum for outcomes → Result
Great for avoiding nullable returns/exceptions for expected failures.
public sealed interface Result<T> permits Ok, Err {} public record Ok<T>(T value) implements Result<T> {} public record Err<T>(String message) implements Result<T> {} public static Result<Integer> safeDivide(int a, int b) { return (b == 0) ? new Err<>("division by zero") : new Ok<>(a / b); } public static String render(Result<Integer> r) { return switch (r) { case Ok<?> ok -> "✅ " + ok.value(); case Err<?> er -> "❌ " + er.message(); }; }
When should you use ADTs? 🎯
- Domain modeling: Encode business rules in the type system (states, commands, events).
- State machines & workflows: e.g., Order = Draft | Paid | Shipped | Cancelled.
- Parsing & validation: return Ok/Err instead of throwing.
- Exhaustive branching: you must handle every case—no forgotten else.
- API design: make illegal states unrepresentable.
Pros ✅
- Correctness by construction: the compiler checks you covered all cases.
- Clarity: readers see all possible shapes of a value in one place.
- Immutability by default with records → easier reasoning, thread-friendlier.
- Less null, fewer fragile strings: explicit alternatives instead.
- Refactor-friendly: adding a new variant breaks switches at compile time (good!).
Cons ⚠️
- More types upfront: small learning curve for teams new to ADTs.
- Framework friction: older libs may need tweaks for record/sealed (e.g., add Jackson modules/annotations).
- Extensibility constraints: sealed is closed on purpose (strong for safety, less open for plugin-style extension).
- Pattern matching requirements: best experience on Java 21+.
Practical tips 🛠️
- Product = “and” → record (validation in the compact ctor).
- Sum = “or” → sealed interface + record cases.
- Prefer exhaustive switch over chains of if.
- Use ADTs at bounded contexts & API boundaries—they shine where correctness matters.
- For rich error handling, model Result, Option, Either as sealed hierarchies (or use a well-known library).
Takeaways 💡
- ADTs = Product (AND) + Sum (OR) to model data precisely.
- In Java, record (product) + sealed + pattern matching (sum) give you the toolkit.
- Use them to encode domain rules, enable exhaustive handling, and reduce bugs.
- You’ll write a bit more types, but ship a lot more confidence.
#️⃣ #Java #ADTs #SealedClasses #Records #PatternMatching #FunctionalProgramming #DomainModeling #CleanCode #SoftwareDesign 🚀