skip to content

What is the tolerant reader pattern in service-to-service integration, and what specifically should a consumer's parsing code do (and not do) to implement it?

level: middleimportance: should knowfreq 55%

answer

  1. ignore unknown fields
  2. extract only what you need
  3. default branch for unknown enum values
  4. no strict schema validation on responses
  5. complements provider-side additive changes

basics

~20 s

A tolerant reader only looks at the specific fields it actually needs from a response and ignores everything else, so it doesn't break when the provider adds new fields or extends the data it sends.

solid answer

~50 s

The tolerant reader pattern (from Martin Fowler) says a consumer should extract only the data elements it actually needs from a provider's response and be indifferent to everything else: unknown fields, added enum values, reordered elements, or extra nesting should all be silently ignored rather than causing a parse failure. Concretely: don't use strict schema validation that rejects unknown properties on responses, don't deserialize into structs/DTOs that fail on unrecognized fields, don't assume field order in arrays or objects, and for enums/status codes, have an explicit default/unknown branch rather than an exhaustive switch that throws on an unrecognized value. This shifts the compatibility burden: instead of the provider needing a perfect contract negotiation with every consumer before adding a field, most additions become invisible non-events to well-built consumers, which is what actually makes 'backward compatible = additive only' work in practice at the client level, not just the server level.

go deeper

for a junior

Can state that a consumer should only read the fields it needs and not choke on extra data, with a simple example like an added JSON field.

for a middle

Names concrete implementation choices - lenient deserialization, default enum branches, no positional array assumptions - and can explain why strict DTOs break on additive changes.

for a senior

Balances tolerant reading against the risk of masking real bugs, pairs it with targeted contract tests on fields that matter, and applies it consistently to enums, error codes, and headers, not just JSON body fields.

for a principal

Sets it as a required consumer-side coding standard across teams, includes it in service templates/linters, and recognizes it as necessary but not sufficient - requiring contract testing as the complementary safety net at org scale.

## What the pattern says The **tolerant reader** pattern, popularized by Martin Fowler, describes how a service acting as a consumer of another service's API should be built so that it survives additive, backward-compatible changes on the provider side without any code change on the consumer's part. The mechanism is deceptively simple: a tolerant reader extracts and depends on only the specific fields, headers, or status codes it actually needs to do its job, and it is deliberately indifferent to everything else in the payload. ## The concrete coding choices In practice this means several concrete coding choices. 1. First, **deserialization should be lenient**: if you're mapping a JSON response into a typed object, use a library configuration that ignores unrecognized properties rather than throwing on them - many JSON libraries default to strict mode and need this explicitly relaxed. 2. Second, **don't validate incoming responses against a strict schema** that forbids additional properties; validation on the consumer side should check only that the fields you need are present and correctly typed, not that the whole payload matches some canonical shape. 3. Third, **never assume positional ordering** of array elements or object keys - always access by key/identifier, not by index or declaration order. 4. Fourth, for enumerated values (status codes, category strings, event types), **always code a default/'unknown' branch** instead of an exhaustive switch that throws on any value it wasn't written to expect; a new enum value the provider adds later should degrade gracefully (e.g., treat an unrecognized order status as 'needs manual review') rather than crash the consumer. ## Why it exists: compatibility is two-sided The reason this pattern exists is that backward compatibility is a two-sided contract: a provider being disciplined about only making additive changes is necessary but not sufficient. If the consumer's code is brittle - built to reject anything it wasn't explicitly told to expect - then even a perfectly additive, spec-compliant change from the provider will break that consumer anyway. The tolerant reader pattern is the consumer-side half of the compatibility discipline that makes the provider-side 'additive only' rule actually pay off. Without it, every provider change, however careful, still requires a synchronized consumer deploy, which defeats the purpose of independent deployability. ## The trade-off The trade-off is that tolerant reading trades **strictness for resilience**, and strictness has real value: a consumer that silently ignores unexpected data can mask genuine provider bugs or contract violations that a stricter consumer would have caught immediately. If a provider accidentally sends malformed or semantically wrong data, a strict consumer fails loudly and immediately; a tolerant consumer might silently misbehave more subtly - for example, treating an unrecognized new 'important' enum value as harmless default behavior when it actually needed special handling. This is why tolerant reading is usually paired with contract tests (like CDC/Pact) on the fields that genuinely matter, rather than being the only compatibility mechanism: contract tests catch the 'did the fields I care about change unexpectedly' problem at build time, while tolerant reading handles the 'harmless new stuff I don't care about' problem at runtime. ## Failure modes, in two opposite directions Failure modes show up in two opposite directions. - **Under-tolerant consumers break on harmless changes**: a classic production incident is a provider adding a new, purely informational field to a response, followed by an unrelated consumer service crashing in production because its strict JSON deserializer or exhaustive switch statement rejected the unfamiliar data - despite the provider having done nothing 'wrong' by additive-only rules. - **Over-tolerant consumers mask real problems**: a consumer that ignores all fields except the two it reads might silently continue operating during an incident where the provider is actually returning corrupted or stale data in fields the consumer should have been validating. ## A concrete scenario A concrete scenario: an inventory service adds a new `reservedQuantity` field to its `/stock/{sku}` response to support a new reservation feature, while the existing `availableQuantity` field is untouched. - A **pricing service** that only reads 'availableQuantity' and was built as a tolerant reader (ignoring unknown JSON properties) keeps working with zero code change or redeploy. - A **poorly built shipping service** that deserializes the response into a strict DTO with a fixed set of fields and no 'unknown property' tolerance throws a deserialization exception on every call the moment the inventory service ships the new field - an outage caused entirely by consumer-side brittleness, not by any provider mistake.

  • Why is the tolerant reader pattern considered the consumer-side half of backward compatibility, rather than something only providers need to worry about?
    Because a provider can follow every rule of additive-only, backward-compatible evolution and still break a consumer if that consumer's parsing code is brittle - for instance, rejecting any response containing an unrecognized field. Tolerant reading is what actually lets additive provider changes pass through harmlessly, so both sides have to hold up their end for real backward compatibility to work in practice.
  • What's the risk of taking tolerant reading too far, and how do teams usually guard against it?
    An overly tolerant consumer can silently ignore data it actually should have reacted to, masking real provider bugs or contract violations instead of failing loudly. Teams typically guard against this by pairing tolerant reading with targeted contract tests on the specific fields the consumer genuinely depends on, so those are still validated precisely, while everything else remains safely ignorable.
  • How should a tolerant reader handle an enum value it has never seen before, such as a new order status?
    It should have an explicit default or 'unknown' branch that degrades gracefully - for example, routing to a manual-review queue - rather than an exhaustive switch statement that throws or crashes when it encounters a value it wasn't written to expect. This lets the provider safely add new enum values over time without every consumer needing a synchronized redeploy.

Like reading a menu and only caring about the three dishes you plan to order - a new dessert added to the bottom of the menu doesn't confuse you, because you never tried to memorize the whole page.

saying these in an interview costs you the question

  • deserializes into strict DTOs that fail on unknown fields
  • uses exhaustive enum switches with no default case for unrecognized values
  • assumes array/object field ordering is stable
  • believes tolerant reading means ignoring validation of the fields it actually depends on
  • thinks tolerant reading is only relevant to REST/JSON and not other RPC formats

context