Many software failures begin far from the place where they are eventually noticed. A malformed value enters through an API, configuration file, message, command, or user action. The program accepts it, passes it through several layers, and fails later when some unrelated operation assumes the value is valid.
By then, the original cause is harder to see.
A useful design principle is to validate data at the boundary where it enters a trusted part of the system. A boundary is any point where code receives information whose assumptions it does not yet control. The goal is not to scatter checks everywhere. It is to turn uncertain input into either a known-valid value or an explicit failure before the rest of the program relies on it.
This article explains how to identify those boundaries, decide what belongs in validation, and avoid both missing checks and excessive defensive code.
Think in terms of trust transitions
Consider an order service that receives this request:
quantity = -3
shipping_method = "express"Deep inside the application, inventory code may reasonably assume that an order quantity is positive. If the request reaches that code unchanged, the inventory component now has two jobs: perform inventory work and defend itself against malformed requests.
The problem grows when every downstream component repeats the same checks.
A cleaner mental model is a trust transition. Before validation, the request is untrusted data. After successful validation, application code can rely on a defined set of properties.
external input
|
v
[ validation boundary ]
|
+---- invalid ----> explicit error
|
v
validated application dataFor the order example, the boundary might establish that:
- quantity is an integer greater than zero;
- shipping method is one of the supported values;
- required identifiers are present and structurally valid.
Once those conditions hold, deeper code does not need to rediscover them at every call site.
Validate what the boundary can actually know
Not every rule belongs in the first validation step.
Suppose a request contains:
customer_id = "C-1042"
quantity = 3The request boundary can check that customer_id has the required shape and that quantity is positive. It may not be able to decide whether customer C-1042 currently exists or whether three units are available without consulting other parts of the system.
This creates an important distinction.
Structural validation checks whether data has an acceptable form. Examples include required fields, ranges, lengths, allowed values, and basic relationships between fields.
Business decisions depend on the current state or rules of the application. Examples include whether an account may place an order, whether stock is available, or whether a transition is allowed from the entity’s current state.
Keeping those responsibilities separate makes failures easier to interpret. A malformed quantity is different from a valid quantity that cannot be fulfilled.
Move validation close to the point of entry
Validation becomes less useful when invalid data can travel a long distance before being rejected.
Imagine this flow:
request -> controller -> service -> pricing -> repositoryIf the repository is the first component to discover that a required product identifier is empty, the resulting error may look like a storage problem even though the request was malformed from the beginning.
Rejecting the value near the request boundary improves the causal connection:
request -> validate -> application service -> pricing -> repository
|
+-> invalid request errorThe benefit is not merely earlier failure. The failure happens in a place that still understands the context needed to explain it.
This is especially valuable when a system crosses asynchronous boundaries. Once malformed data has been written to a queue, event log, or persistent store, the original caller may no longer be available to correct it. Validating before crossing that boundary can prevent invalid records from becoming somebody else’s debugging problem.
Do not confuse boundary validation with checking everything everywhere
A common reaction to invalid input is to add defensive checks to every function.
For example, suppose application code has already converted a request into a validated OrderRequest. If every internal function still checks that quantity > 0, the same rule is duplicated throughout the codebase.
That duplication has costs. Rules can drift, error messages can disagree, and developers can no longer tell which layer owns the guarantee.
Instead, make the guarantee visible in the design:
parse request
-> validate request
-> create validated order command
-> execute business operationThe exact representation depends on the language and codebase. It might be a dedicated type, constructor, factory function, or simply a clearly documented application-layer contract. The important point is that successful validation should establish something downstream code can intentionally rely on.
This does not mean internal code should never check assumptions. Components still need to protect boundaries they own, especially when they are reusable, independently callable, or exposed to multiple callers. The goal is to avoid blindly repeating checks whose guarantees have already been established.
Choose errors that preserve the reason for rejection
Validation is most useful when its failure tells the caller what was wrong.
Compare these responses:
invalid requestand:
quantity must be greater than zeroThe second response identifies the violated condition. That makes the error useful to callers, logs, tests, and developers.
However, validation errors should describe the contract rather than leak implementation details. A caller usually needs to know that a value is unacceptable, not which internal helper function or regular expression rejected it.
For multiple independent fields, collecting several validation problems can be helpful because the caller can correct them together. For operations where later checks depend on earlier ones, stopping after the prerequisite fails is clearer. There is no universal rule that validation must always fail on the first problem or always collect every problem.
Choose the behaviour that matches the boundary’s contract.
Validate again when the trust boundary changes
A value that was validated earlier is not automatically trustworthy forever.
Consider a service that reads an event produced by another application. The producing application may validate the event before publishing it, but the receiving service still crosses its own trust boundary when it consumes that event.
There are several reasons to validate again:
- producers and consumers can be deployed at different times;
- stored or queued data may outlive the code that created it;
- another producer may eventually send the same message type;
- schemas and business assumptions can evolve independently.
The consumer should therefore validate the assumptions it requires instead of depending entirely on another process having done so correctly.
This is not pointless duplication. The two checks protect different boundaries.
Separate validation from normalization
Systems often transform input while validating it. That can be useful, but the two operations answer different questions.
Normalization converts equivalent representations into a chosen form. Trimming surrounding whitespace from a value may be normalization.
Validation decides whether the resulting value is acceptable.
For example:
input: " EXPRESS "
normalize: "express"
validate: value is one of {"standard", "express"}The order matters because normalization changes what is being checked. It should be intentional and documented by the boundary contract.
Be cautious with transformations that silently change meaning. Replacing an unknown value with a default may hide an upstream mistake rather than normalize harmless variation. When the software cannot confidently infer the caller’s intent, explicit rejection is usually easier to reason about than silent correction.
Avoid validation rules that become stale copies
Some validation failures come from duplicating knowledge that belongs elsewhere.
Suppose a boundary hard-codes a list of subscription plans while the domain component maintains a separate list. The two lists can diverge. A new plan might be accepted by the domain but rejected at the boundary, or the reverse.
Prefer a single source of truth for rules that must stay synchronized. The boundary can still perform validation, but it should derive the decision from the authoritative rule or shared representation rather than maintain an independent copy.
The same principle applies to limits, state transitions, feature availability, and other rules that change with the application.
Know what validation cannot guarantee
Boundary validation reduces uncertainty; it does not make later failure impossible.
A request can be valid when checked and still fail later because state changes. Inventory may disappear between validation and reservation. A referenced resource may be deleted. A downstream dependency may become unavailable.
That means validation should not be used as a substitute for handling operational failures or enforcing invariants at the component that owns them.
A useful division of responsibility is:
boundary validation
-> Is this input acceptable to process?
business logic
-> Is this operation allowed in the current state?
resource-owning component
-> Can the required state change be committed correctly?Each layer answers a different question. Treating them as interchangeable creates false confidence.
Use boundary validation when uncertainty enters
Boundary validation is particularly valuable when data arrives from outside the assumptions of the current component: network requests, command-line arguments, environment configuration, files, queues, external services, plugins, or separately deployed modules.
It is less useful to add a new validation layer between two small internal functions when both already operate under the same well-defined contract. In that case, clearer types, constructors, tests, or module boundaries may communicate the guarantee better than repeated runtime checks.
A practical question is:
At what point does this code first gain enough context to reject invalid data clearly?
That point is often the right place for validation.
Conclusion
Good validation is not about distrusting every value at every line of code. It is about recognizing where uncertainty enters a system and converting that uncertainty into an explicit contract.
Validate structural assumptions near the boundary that understands them. Keep business decisions with the components that own those rules. Preserve useful failure reasons, avoid duplicated sources of truth, and validate again when data crosses a new trust boundary.
When those responsibilities are clear, invalid data travels less far, failures stay closer to their causes, and the trusted interior of the system becomes simpler to reason about.