How to decide where validation belongs.

•

•

Validate at the Edge, Trust the Inside

I once spent three days debugging a race condition in a logistics system that only triggered when a specific combination of malformed JSON hit our API gateway. We had layers of validation—some in the frontend, some in the middleware, and some deep in the database constraints—yet the system still let garbage data slip through the cracks. It wasn’t a lack of code; it was a lack of clarity. Most senior devs will give you a lecture on architectural patterns or design principles, but they rarely tell you the practical reality of how to decide where validation belongs when your production database is screaming at you.

I’m not here to sell you on a specific framework or a “perfect” way to structure your DTOs. Instead, I want to look at the trade-offs of where you actually place your logic. I’ll walk you through the messy middle ground between “validating everything at the edge” and “trusting the database to catch it,” based on the times I’ve been wrong. My goal is to help you build a system where you can actually sleep through the night because you know exactly which layer is responsible for keeping the junk out.

Table of Contents

Navigating Social Validation Layers Under Extreme Pressure

When the pager goes off and the incident commander is breathing down your neck, the technical problem is only half the battle. I’ve sat in bridge calls where the pressure was so high that people stopped communicating facts and started seeking constant reassurance. This is where navigating social validation layers becomes a survival skill. You see it constantly: engineers looking for external validation from a lead just to confirm they aren’t making a mistake, even when the logs are screaming the answer. It’s a massive drain on cognitive load. If you spend your energy trying to manage everyone’s anxiety instead of fixing the database, the MTTR (Mean Time To Recovery) is going to skyrocket.

The trick is distinguishing between the need for technical consensus and the trap of seeking constant approval. In a high-stakes environment, you have to maintain a level of psychological safety and trust that allows for quick, decisive action without the need for a committee vote on every command. I’ve learned that the most effective responders are those who can provide clear, objective feedback without getting bogged down in the emotional turbulence of the room. You aren’t there to soothe everyone’s ego; you’re there to stabilize the system. If you can’t separate the technical truth from the social noise, you’ll find yourself stuck in a loop of indecision while the production environment burns.

The Cost of Choosing Internal vs External Validation

The Cost of Choosing Internal vs External Validation

The mistake I see most often isn’t a lack of validation logic; it’s a lack of understanding about where the cost is actually paid. If you dump everything on the client side, you’re essentially asking the user to do your job for you. It feels snappy and responsive, but it’s a facade. The moment someone bypasses your UI with a `curl` command or a rogue script, your database becomes a landfill of corrupted state. You’ve optimized for a smooth user experience at the expense of system integrity, and that’s a debt that always comes due during a migration or a scale-up event.

On the flip side, relying solely on heavy, server-side validation for every single keystroke is its own kind of nightmare. It turns your backend into a bottleneck and creates a sluggish, frustrating interface. It’s a bit like the tension between internal vs external validation in a psychological sense; if the system only feels “right” when the server gives a thumbs up, the user experience feels disconnected and heavy. You end up with a system that is technically “correct” but practically unusable. I’ve learned that the goal isn’t to pick a side, but to decide where the source of truth lives versus where the feedback loop lives.

A few rules of thumb I use when the architecture starts looking messy

  • Validate at the boundary, but don’t stop there. I’ve seen too many systems crash because the API layer checked the email format, but the database layer assumed the string was already sanitized. Treat every internal service call as if it’s coming from a malicious user; it’s the only way to sleep through the night.
  • If the validation logic requires a database lookup, it’s business logic, not input validation. Don’t let your request parsers start making network calls. It’s a recipe for latency spikes and a very confusing trace when the “input validation” step suddenly hangs because the connection pool is exhausted.
  • Favor “Parse, don’t validate.” Instead of passing a raw string around and checking if it’s a valid UUID every time a function touches it, transform that string into a specific `UserId` type as soon as it enters your system. If the type exists, the data is valid. It turns a runtime headache into a compile-time certainty.
  • Distinguish between “is this well-formed” and “is this allowed.” Checking if a number is between 1 and 100 is a structural check; checking if that user has enough balance to spend that 100 is a state check. Mixing these two up is how you end up with race conditions that are impossible to replicate in staging.
  • Document the “why” of your validation rules in the code, not just the “what.” I don’t need a comment saying `if (age < 18)`. I need to know that we are enforcing this because of a specific compliance requirement from the legal team in 2022. Without that context, the next engineer will "optimize" it right out of existence.

What I've learned from the wreckage

Don’t treat validation as a single “layer” to be checked off; it’s a series of gates. If you only validate at the API edge, your database will eventually ingest garbage when a background job or a migration script bypasses that gate.

Choose your battle based on the cost of failure. If a bad input means a UI error, keep it at the edge. If a bad input means a corrupted ledger or a broken state machine, it belongs in the core domain, no matter how much it slows down the development cycle.

Write your validation rules as part of your business logic, not as a side effect of your framework. If you can’t explain the validation rule without mentioning a specific library or a JSON schema, you haven’t actually defined what your data is supposed to look like.

The Final Trade-off

Architectural decisions and The Final Trade-off.

At the end of the day, there is no “correct” layer for validation—only a set of trade-offs that you have to live with when the logs start screaming. You can push everything to the edge to keep your core logic clean, but you’ll pay for it in complex error handling and fragmented state. Or, you can enforce everything at the center, which keeps your domain models pure but often results in clunky, unhelpful API responses that leave frontend developers guessing. The goal isn’t to find a perfect architecture; it’s to ensure that the decision you make is intentional and that you aren’t just blindly following a pattern you saw in a blog post three years ago.

I’ve spent enough time fixing broken state in production to know that the most dangerous code is the kind that assumes it knows where the truth lives. Don’t aim for a system that is theoretically perfect; aim for one that is predictable under pressure. When you decide where a check belongs, ask yourself if you’ll be able to debug it at 2:00 AM when the data is malformed and the service is timing out. If you can answer that, you’re probably on the right track. Now, go write the commit message and actually build the thing.

Frequently Asked Questions

If I move validation to the domain layer to keep it "pure," how do I handle the inevitable performance hit when I'm checking massive batches of data?

The “pure domain” approach is great for correctness, but it’s a trap if you try to run it blindly against a million-row batch. I’ve seen this before: the domain model is perfect, but the database is screaming because you’re pulling every entity into memory just to check a single field.

At what point does "defensive programming" at every single layer become just redundant code that makes the system impossible to refactor?

It becomes redundant the moment you stop trusting your own types. If your internal service layer is checking if an ID is a UUID for the tenth time, you aren’t being “defensive”—you’re just writing boilerplate that hides the actual business logic. I’ve seen codebases where refactoring a single field felt like a game of Minesweeper because every layer had its own redundant validation logic. Validate at the boundaries; trust your types in the core.

How do you manage the tension between strict backend validation and the need to keep the frontend snappy and responsive for the user?

I’ve seen this play out: the backend is a fortress, but the UI feels like it’s wading through molasses because every keystroke triggers a round-trip to a database.

When a validation rule changes—like a new compliance requirement in payments—how do I avoid having to touch five different services just to update one check?

The temptation is to scatter these rules everywhere for “efficiency,” but that’s how you end up in dependency hell. When a compliance rule changes, you shouldn’t be playing whack-a-mole across five microservices. I’ve learned the hard way: if the logic is business-critical, it belongs in a single, authoritative source—usually a dedicated domain service or a shared library that isn’t a “junk drawer.” Centralize the rule, version the contract, and let the other services just consume the result.

Tomasz Wierzbicki

About Tomasz Wierzbicki

I am not interested in arguing about frameworks. I am interested in why the thing broke at two in the morning and what would have prevented it. Most of what I know came from being wrong in production and writing it down afterwards.