Parse at Boundaries to Protect Domain Invariants
A request arrives with a string that is supposed to be an order quantity. One function checks that the string contains a number. Another checks that the number is positive. A third assumes both checks already happened. Months later, a new caller reaches the third function directly and passes zero.
The problem isn’t simply missing validation. The program keeps carrying a weak representation after it already knows something stronger about the value.
A useful design technique is to parse at boundaries: when data enters a trusted part of the system, convert it from a loose external representation into a domain value whose construction enforces the rules that internal code depends on. If conversion fails, reject the input there. If it succeeds, downstream code receives evidence that those rules were satisfied.
This article explains the mental model, shows where it reduces repeated checks, and covers the limits that matter in real systems.
Validation asks a question; parsing changes the representation
Consider a function that receives a quantity as an integer:
function ship(quantity):
if quantity <= 0:
return error("quantity must be positive")
...The check is correct, but quantity remains an ordinary integer afterward. Nothing prevents another function from accepting the same broad type and forgetting the check.
Now introduce a domain value called PositiveQuantity. Its constructor accepts an integer only when the integer is greater than zero:
function parsePositiveQuantity(value):
if value <= 0:
return error("quantity must be positive")
return PositiveQuantity(value)Code that needs a valid quantity can now require PositiveQuantity rather than an unrestricted integer:
function ship(quantity: PositiveQuantity):
...The exact syntax depends on the language. The design does not.
The first version asks, “Is this integer valid here?” The second turns a broad value into a narrower domain representation. Once that conversion succeeds, code operating on PositiveQuantity does not need to rediscover the same fact.
That is the central mental model:
external representation -> parse -> domain representation
|
failure stops hereParsing is useful when success establishes an invariant: a condition that the rest of a piece of code is allowed to rely on.
Put the conversion where trust changes
A boundary is any place where data moves from a context with weaker guarantees into one with stronger assumptions. An HTTP handler is an obvious example, but boundaries also appear when reading a file, consuming a message, loading loosely constrained persisted data, accepting command-line input, or calling into a domain module from generic application code.
Suppose an order endpoint receives this payload:
{
"productId": "P-104",
"quantity": 3
}The transport layer can decode JSON into primitive values. That only establishes that quantity is representable as a number according to the decoder’s rules. It does not establish the business rule that an order quantity must be positive.
A boundary adapter can perform the next conversion:
request JSON
|
v
transport values
|
| parse product ID
| parse positive quantity
v
domain commandIf quantity is 0, construction of the domain command fails before the command reaches order logic. If it is 3, the order logic receives a value with the invariant already established.
This separation makes cause and effect clearer. The boundary owns interpretation of weak input. The domain owns the rules that define valid domain values. Internal operations can then work with those values instead of repeatedly defending themselves against representations they should never receive.
Keep the invariant close to the type that promises it
Moving checks to a boundary does not mean copying business rules into every controller or message consumer. That would create a different maintenance problem.
The boundary should invoke domain construction; it should not independently redefine the domain rule.
For example, avoid this arrangement:
HTTP handler: quantity > 0
message handler: quantity >= 1
batch importer: quantity != 0These expressions happen to accept the same positive integers in many cases, but they encode the rule separately and can drift as requirements change.
Prefer one domain operation:
PositiveQuantity.parse(rawQuantity)Every adapter can call that operation and translate its failure into an appropriate boundary response. An HTTP adapter might produce a client error. A message consumer might reject or quarantine a malformed message. A batch importer might record the bad row and continue. The domain rule stays the same even though failure handling differs by context.
This distinction is useful: domain construction decides whether a value is valid; the boundary decides what to do when construction fails.
Parse enough to support the decisions that follow
It is possible to take the idea too far and create a new type for every field. The goal is not maximum type count. The goal is to replace representations that permit states your internal code is not prepared to handle.
A dedicated domain value tends to earn its place when at least one of these is true:
- several operations depend on the same invariant;
- forgetting the invariant would produce incorrect behavior rather than a harmless formatting issue;
- the value has domain-specific operations or comparisons;
- multiple primitive values have the same underlying type but different meanings;
- the rule is stable enough that callers should not choose their own interpretation.
A temporary page size used by one small function may be fine as an integer with a local check. An account identifier passed through dozens of operations may benefit from a dedicated representation even if its initial validation is simple.
The simpler approach is better when a rule is genuinely local. Wrapping every intermediate number and string can make code harder to navigate without providing a meaningful guarantee.
Not every rule can be captured during parsing
Some invariants depend only on the value being constructed. PositiveQuantity can establish positivity without consulting anything else. These are good candidates for boundary parsing.
Other rules depend on current system state. An order may require that a product exists, has enough stock, and can be sold in the customer’s region. A value type cannot permanently guarantee those facts because they can change after the value is created.
It helps to separate two kinds of checks:
structural or intrinsic rule
"quantity is positive"
-> establish during construction
state-dependent rule
"three units are currently available"
-> evaluate when making the business decisionTrying to encode a changing fact as if it were a permanent property creates a false guarantee. A successful stock check at 10:00 does not prove stock is still available at 10:05 unless the system also reserves it or provides some other concurrency guarantee.
Parsing narrows what a value can mean. It does not freeze the outside world.
Be precise about what construction guarantees
A domain type is useful only if its name and construction rules match the promise callers infer from it.
Suppose EmailAddress merely checks that a string contains @. Naming the result VerifiedEmailAddress would be misleading because no verification with the mailbox owner occurred. Even ValidEmailAddress may promise more than the parser can establish if the application only checks a small syntactic subset.
Prefer names tied to guarantees you can actually defend. If the system needs several levels of assurance, represent them separately when the distinction affects behavior.
The same rule applies to normalized data. If a parser trims whitespace or changes case, ask whether that transformation is valid for the domain rather than treating normalization as automatically safe. Parsing should establish known rules, not silently invent them.
Watch for escape hatches that recreate invalid states
The pattern loses much of its value if internal code can freely bypass construction.
For example, a PositiveQuantity type whose fields are publicly writable may start valid and later become zero. A deserializer that can instantiate the type without running its checks can have the same effect. The mechanism varies by language, but the required property is consistent: code that obtains the domain value should not be able to violate the invariant through ordinary supported operations.
Mutation needs particular care. If an operation changes the wrapped value, it must preserve the invariant or return a new checked value. For quantities, subtraction might therefore fail when the result would be zero or negative rather than leaving the object in an invalid state.
Persistence is another boundary worth noticing. Data written by an older application version, manually edited records, or corrupted storage may not satisfy today’s assumptions. Reconstructing domain values from storage should preserve the same guarantees unless the storage layer itself provides constraints strong enough to justify trusting the data.
Parsing changes where errors appear
Rejecting invalid data earlier is usually easier to reason about, but it changes error handling. A boundary now needs a deliberate way to report construction failures.
Keep those failures specific enough to be useful without leaking internal details. A parser can distinguish an absent value from a malformed value when callers need that distinction. The adapter can then map domain failures into the vocabulary of its protocol or workflow.
Do not use domain parsing to hide infrastructure failures. “Quantity is not positive” is a domain construction failure. “Could not read the request body” or “dependency timed out” belongs to a different failure category. Combining them into one generic invalid-input path makes diagnosis harder and can lead callers to retry errors that will never succeed, or fail to retry transient errors that might.
Use parsing where stronger guarantees simplify the inside
A good test for this design is to look at the code after the boundary.
If internal functions still contain repeated checks for conditions that construction supposedly guarantees, either those checks are redundant or the domain representation is not carrying the guarantee you need. If callers frequently unwrap the value, manipulate the primitive directly, and reconstruct it, the abstraction may be too restrictive or placed at the wrong level.
The useful outcome is visible in ordinary code: function signatures describe stronger assumptions, invalid input is rejected in a small number of predictable places, and business logic spends more time making business decisions than rechecking its inputs.
Start with one recurring invariant that is currently checked in several places. Give it a domain construction path, route untrusted input through that path at the boundary, and make the internal operation accept the stronger representation. If that removes repeated defensive checks without hiding necessary state-dependent decisions, the boundary has become more useful.