Stop Validating Your Data. Your Types Are Begging for a Real Job.

You know that sinking feeling. It’s 2 AM, you’re staring at a stack trace, and the root cause is a single null that slipped through seventeen layers of defensive if statements. You’ve been there. I’ve been there. And I’m here to tell you: it’s not your fault — but it is your data model’s fault.

Every time you write if (user != null) or if (email matches regex), you’re not solving a problem. You’re confessing that your types failed to capture the domain. Runtime validation is a confession that your data model is a liar. The industry has been selling you a comfortable lie: ‘fail fast’, ‘defensive programming’, ‘validate early’. But these are band-aids on a broken bone. The real solution is to make invalid states unrepresentable by construction.

This is the core of constructive data modeling — a paradigm shift that moves the burden of correctness from runtime checks to the type system itself. Imagine a compiler that mathematically guarantees your data is valid before a single line of code runs. That’s not a pipe dream; it’s what Alexis King demonstrated in her seminal talk, ‘Parse, Don’t Validate’. She showed us that the compiler can be your best friend — if you let it.

But here’s the twist: most developers resist this because it feels ‘too rigid’. They think domain modeling is over-engineering. They’d rather write 500 lines of validation than design three types that make the 500 lines unnecessary. That’s not pragmatism — that’s learned helplessness.

Let me give you a concrete example. Imagine a payment system. Most teams would write: if (amount > 0 && currency != null && ...). A constructive model, on the other hand, would define a NonNegativeMoney type that literally cannot be negative, and a SupportedCurrency type that only accepts valid ISO codes. The compiler becomes your enforcer. No more ‘unexpected’ negative prices. No more ‘invalid currency’ runtime errors. The state is impossible.

This isn’t just for Haskell or Rust. You can apply this in C#, TypeScript, even Java — with the right discipline. The pattern is universal: parse the raw input into a strongly-typed domain object at the boundary, and then never check it again. Parse once, trust forever.

The irony is that the most common objection is ‘it’s too much work’. But consider the cost of a single production outage caused by a validation bug. The hours of debugging, the lost revenue, the eroded trust. Suddenly, an afternoon of careful type design looks like the cheapest insurance you’ll ever buy.

If you’re constantly validating, you’re not being careful — you’re being lazy with your types. Every unnecessary if is a debt you’re deferring to a future version of you. And that future version of you is exhausted, running on caffeine, and about to add another if instead of fixing the root cause.

Don’t validate. Model. The compiler is waiting to do the heavy lifting — you just have to stop treating it like a glorified syntax checker. Start treating it like a contract enforcer. Your future self will thank you.

FAQ

Q: Isn't this just for functional programming? I use C# and Java.

A: Not at all. While functional languages like Haskell and Rust make it natural, the principle applies to any language with a decent type system. C# with record types, discriminated unions, and nullable reference types can get you most of the way. Java with sealed classes and pattern matching works too. The key is discipline: parse at the boundary, then never check again.

Q: What's the first step to adopt constructive data modeling in my existing codebase?

A: Start small. Pick one module that has the most validation logic. Identify the core domain types (e.g., Email, UserId, Price). Refactor them into types that can't represent invalid states. Then delete the corresponding validation code. Test it. You'll see the cognitive load drop immediately. Rinse and repeat.

Q: Doesn't this make the code too rigid? What if the domain rules change?

A: It's not rigid; it's explicit. When domain rules change, you change the type definition — and the compiler tells you exactly where your code breaks. That's a feature, not a bug. With runtime validation, you'd have to hunt down every scattered check. With types, you get a single source of truth and a compiler that does the hunting for you.

📎 Source: View Source