Software Architecture / Foundations: promises, structure, and ownership / Lesson 5
Specify contracts that survive independent change
Describe request meaning, compatibility, errors, and uncertain outcomes beyond a successful response schema.
On this page
Lesson goals
- Distinguish syntax compatibility from preserved behavior.
- Define additive evolution, validation boundaries, and error semantics.
A contract includes more than field names
An interface contract describes what a caller may request, what outcomes mean, and what the provider guarantees under stated conditions. For reservations, units, identity, duplicate handling, authorization scope, and time representation matter as much as JSON field types. A response with status confirmed must correspond to the authoritative business state, not merely acceptance into a queue. Document whether the caller can cancel, query, or retry an uncertain operation. A schema validator can check structure but cannot establish that the implementation honors these semantic promises.
Make ambiguous values explicit
The fixture uses an opaque workshop identifier, an integer seat count, and a stable client request identifier. It avoids accepting a client-supplied price as authoritative. A real implementation still needs length limits, allowed ranges, identity binding, and an authorization decision. If the same request identifier arrives with different business input, rejecting the conflict is safer than silently reusing an unrelated result. State the retention window for deduplication records so a caller knows when retrying an old identity no longer carries the same guarantee.
{"request_id":"req-17","workshop_id":"ws-42","seats":1}
This request is syntactically valid JSON. Its business validity depends on the provider contract; the identifier does not replace authentication or authorize access to workshop ws-42.
Separate rejection, failure, and uncertainty
A known business rejection such as capacity exhausted differs from an internal server failure and from a connection timeout after an unknown commit. Give callers stable machine-readable error codes and useful safe descriptions. Do not require clients to parse human prose, database exceptions, or translated text. Define which failures are retryable and under what identity and deadline. HTTP status codes communicate broad protocol meaning; the domain error supplies additional context. A transport success carrying an unexplained error flag can confuse monitoring and client behavior unless deliberately specified.
Treat optional fields and enum growth carefully
Adding a field can be compatible when old consumers ignore unknown fields and new consumers tolerate its absence. That is an assumption to test, not a universal property of JSON. Adding an enum value can break exhaustive client logic; changing a field from absent to null may alter meaning. The proposed response adds an optional display label while preserving identifiers and confirmed semantics. Test an older consumer against the new response and a newer consumer against the old response when rolling deployments create both combinations.
{"reservation_id":"r-8","status":"confirmed","display_label":"Workshop booking"}
Compatibility depends on consumer behavior. display_label is illustrative optional presentation data; reservation_id and status retain the earlier meaning, and no consumer test is implied by this document.
Evolve the contract with migration evidence
Version a contract when a breaking change cannot be introduced safely through compatible stages. Publish the replacement behavior, support overlap, migration checks, and retirement ownership. Distinguish interface version from application release version: a new build need not require a new public API. Observe actual consumer use where authorized and provide a rollback path that respects data already written in the new form. A deprecation notice alone does not prove every consumer has migrated, especially when offline clients or scheduled jobs call infrequently.
Common errors and fixes
Clients parse a translated error sentence.
- Why it happens
- Presentation text became the machine contract.
- How to fix it
- Add stable error codes and keep localized text separate.
- Prevent it next time
- Test errors through code and semantics, not wording.
A new enum value crashes old consumers.
- Why it happens
- Additive schema changes were assumed behaviorally harmless.
- How to fix it
- Define unknown-value handling or introduce a compatible migration.
- Prevent it next time
- Include old/new consumer combinations in contract review.
A timeout is reported as a definite rejection.
- Why it happens
- Transport evidence was confused with business outcome.
- How to fix it
- Expose an outcome query or safe idempotent retry mechanism.
- Prevent it next time
- Model uncertain outcomes explicitly in client workflows.
Check your understanding
1. Can a request_id field alone make reservations idempotent?
Show worked solution
No. The provider must bind the identity to the relevant caller and request content, store its outcome atomically with the reservation, and define concurrency and retention behavior. The client must reuse the identity for the same operation. Merely accepting a string leaves duplicate execution and conflicting payloads possible.
2. A developer renames confirmed to accepted without changing the schema type. Is this compatible?
Show worked solution
It may break consumers that match the original value and can alter the promise from a completed reservation to an accepted request. Compare both syntax and meaning. Preserve the old value during a migration or explicitly version a breaking semantic change, and provide tests that demonstrate the chosen transition.
Key takeaways
- Compatibility is a property of interacting implementations and meanings.
- A contract must explain failures and uncertainty as carefully as success.