The Single Source of Truth: Why the HTTP Specification Must Be Derived, Not Written

Share
The Single Source of Truth: Why the HTTP Specification Must Be Derived, Not Written

Executive Overview

In modern software engineering, the single greatest silent killer of velocity is not complex algorithms or scaling bottlenecks; it is configuration drift. Systems fail, integrations break, and developers waste countless hours debugging production environments—not because code is written poorly, but because different documentation artifacts describing the exact same interface quietly fall out of sync.

In microservices architectures, this vulnerability usually manifests in the "three-file problem": a protocol buffer schema defines an internal remote procedure call (RPC), a separate OpenAPI document manually tracks the public-facing HTTP surface, and a human-maintained wiki page attempts to summarize both for client teams. On day one, all three documents align. By day ninety, they are fundamentally contradictory.

Anton, a software engineer specializing in PHP, Symfony, and Go—currently orchestrating the decomposition of a live PHP monolith into distributed Go services—argues that this manual synchronization is an entirely avoidable design flaw. In his ongoing series exploring the road from raw business requirements to immutable technical contracts, he outlines a radical yet profoundly logical stance: the contract is declared in the schema, and nowhere else.

By leveraging Protocol Buffers (.proto) as the sole source of truth and utilizing annotations to automatically derive HTTP routing, client libraries, server stubs, and interface documentation, engineering teams can eliminate manual synchronization entirely. In this architecture, drift is no longer a silent nuisance—it is an automated build failure.


Detailed Chronology: The Anatomy of a Derived Contract

To understand the philosophy of derived specifications, one must first look at how services historically evolved and where traditional contract management breaks down.

The Pitfalls of Hand-Written Interfaces

Traditionally, building a microservice requires developers to define behavior across multiple disparate layers. A backend engineer writes the core service logic, manually crafts an OpenAPI or Swagger file to expose the service over HTTP, updates internal Postman collections, and writes human-readable documentation.

The Contract Is a .proto File

This process relies entirely on human discipline. When a field is added to a payload or a refusal code is updated, the engineer must remember to update every single manual representation of that interface. If they forget—which invariably happens during high-pressure sprint cycles—the documentation lies. Clients build against stale specifications, integration tests pass against outdated mocks, and production systems crash upon encountering unexpected payloads.

The Shift to Schema-First Declarations

In Anton’s architecture, this workflow is inverted. Nothing about an interface is ever described by hand in a secondary document. Every service-to-service call, message format, validation bound, and refusal code is codified in a single .proto file stored within a centralized contract repository.

Rather than maintaining a separate HTTP specification, the HTTP role of a handle is embedded directly into the schema via standard HTTP annotations (such as Google’s standard API configuration annotations). The build pipeline then automatically compiles this singular file into:

  1. Typed gRPC client and server stubs.
  2. Fully compliant, auto-generated HTTP specifications and API gateways.
  3. Machine-checked validation logic.

As a result, the HTTP specification is treated as a compiled artifact—much like a binary executable—rather than source code. It cannot drift because it cannot be manually edited.

                    .proto (one file)
        messages · calls · required fields · bounds · refusal codes
                              │
              ┌───────────────┴───────────────┐
              ▼                               ▼
         gRPC call                       HTTP handle
         typed client                    role marked by an annotation
         typed server                    spec derived from this same file

         ❌  hand-written HTTP spec, kept in step by a person

Supporting Context & Metrics: Proving the Principle with Code and Catalogs

The philosophy of machine-derived documentation is not limited to API contracts; it applies to any structural metadata within modern cloud-native systems. Anton traces the genesis of this insight back to two of the most historically neglected operational artifacts in software engineering: environment variables and system metrics.

Eradicating Drift in Configuration and Observability

In legacy applications, environment variables and exported metrics are typically documented in markdown files, READMEs, or external configuration wikis. These lists inevitably suffer from rapid decay; developers add new configuration flags or register new Prometheus metrics in code, completely forgetting to update the corresponding documentation.

The Contract Is a .proto File

To combat this, modern build pipelines can extract these definitions directly from the codebase via static analysis.

  • The Environment Catalog: Rather than maintaining a manual list of required environment variables, a repository audit tool scans the codebase and generates an environment catalog. This catalog explicitly records defined_in metadata—pointing directly to the file and line number where each variable is instantiated, separating platform-level variables from service-specific configurations.
  • The Metrics Snapshot: Similarly, metrics snapshots are compiled automatically, capturing every registered metric alongside dynamic factories instantiated at runtime.
Artifact Entries Where they come from An entry records
Environment catalog 59 variables 45 platform · 14 service config where it is declared
Metrics snapshot 67 entries 59 platform · 6 service · <dynamic> how it is registered

Enforcing Rules via Red Builds

The critical differentiator in this methodology is the enforcement mechanism. Drift is not flagged by a polite linter warning or discussed during code reviews; it is a hard build failure.

Whenever a Go source file, manifest, module file, or snapshot changes, the CI/CD pipeline executes the generator in strict --check mode. If the code and the derived artifacts are out of sync, the build goes red, halting the deployment pipeline instantly.

The Contract Lifecycle: Designing Before Naming

Because the schema serves as the immutable single source of truth, establishing its structure requires rigorous upfront planning. However, because edits to a schema file are cheap (costing mere lines of text) compared to edits post-generation (which ripple across client SDKs, tests, and documentation), upstream planning is heavily emphasized.

Before a single field is named or typed, developers must outline five fundamental operational cases:

  1. Success: The standard response payload and its contents.
  2. Refusal: Explicit error codes and how the caller handles each one.
  3. Conflict: Handling scenarios where competing inputs arrive simultaneously.
  4. Empty Response: Defining an empty list as a valid state rather than an error condition.
  5. Page Boundary: Establishing deterministic pagination mechanics (cursors, limits, and termination conditions).

A Canonical Protocol Buffer Example

Consider a neutral domain package defining a paginated entity list. The .proto schema encapsulates the gRPC contract, HTTP routing annotations, payload bounds, and refusal enumerations in one unified block:

The Contract Is a .proto File
syntax = "proto3";

package example.v1;

import "google/api/annotations.proto";

service EntityService 
  // The HTTP role is declared on the method; the HTTP spec is derived from it.
  rpc ListEntities(ListEntitiesRequest) returns (ListEntitiesResponse) 
    option (google.api.http) = 
      get: "/v1/entities"
    ;
  


message ListEntitiesRequest 
  string parent_id  = 1;  // required; returns a refusal, not an empty list, when missing
  int32  page_size  = 2;  // 1..200, clamped server-side; 0 triggers default
  string page_token = 3;  // cursor from previous response; empty fetches page one


message ListEntitiesResponse 
  repeated Entity entities        = 1;  // an empty list is a valid answer
  string          next_page_token = 2;  // empty only when this is the final page


enum RefusalCode 
  REFUSAL_CODE_UNSPECIFIED        = 0;
  REFUSAL_CODE_PARENT_NOT_FOUND   = 1;
  REFUSAL_CODE_PAGE_TOKEN_INVALID = 2;
  REFUSAL_CODE_SELECTOR_CONFLICT  = 3;

In this model, four of the five core cases are immediately apparent without parsing a single line of business logic implementation. Furthermore, breaking changes are intercepted by contract-testing tools before a single line of service code is written.


Official Statements & Engineering Trade-Offs

While the benefits of derived specifications are profound, adopting a strict single-source-of-truth paradigm introduces distinct operational costs that engineering leaders must weigh carefully.

The Cost Matrix

  1. Tooling Overhead: Teams must maintain and master code generation pipelines, ensuring that protobuf compilers and linting tools remain synchronized across developer environments and CI runners.
  2. Strictness Friction: Developers accustomed to loose, dynamic JSON APIs may find the rigidity of static proto schemas and strict backward-compatibility rules restrictive during early prototyping phases.
  3. Upstream Discipline: Designing robust schemas requires deeper upfront architectural alignment before writing implementation logic.

When NOT to Use Derived Contracts

Recognizing the limits of any architectural pattern is vital to its successful deployment. Anton explicitly outlines scenarios where implementing a heavy contract-first, derived-specification pipeline is unnecessary and counterproductive:

  • Single-Consumer Monolithic Boundaries: An interface that possesses exactly one consumer residing entirely within the same binary does not require externalized schema contracts.
  • Ephemeral Internal Handles: One-off internal utilities or scripts with an expected lifespan of less than a week do not warrant the overhead of formal contract generation.
  • Exploratory R&D: Genuinely experimental work where the shape of the data domain is completely unknown and early iterations are explicitly intended to be discarded.

Data vs. Contract: Knowing the Difference

A common architectural pitfall is over-engineering schemas by codifying dynamic operational data into static enums. Anton draws a sharp line between what belongs in a schema contract and what belongs in database storage:

  • When to use a Dictionary (Data): Categories, statuses, types, or taxonomic labels that expand frequently without requiring changes to conditional branching logic should remain database-backed dictionary rows and string codes. Adding a new category requires no contract changes, no code regeneration, and no deployment rollout.
  • When to use a .proto Enum (Contract): Reserved strictly for states and behaviors that couple directly to application logic—where introducing a new member requires developer branches to explicitly handle the new condition.

The guiding test is simple: If a new member arrives, does code logic have to change? If no, it belongs in data. If yes, it belongs in the schema contract.


Future Outlook

As enterprise software systems continue to scale toward distributed microservices and polyglot architectures, the traditional reliance on manual documentation and hand-maintained API specifications is becoming unsustainable.

The Contract Is a .proto File

The movement toward derived specifications represents a maturing of engineering discipline—shifting trust away from human memory and placing it firmly into automated pipelines. By treating specifications as compiled outputs rather than hand-crafted prose, teams can eradicate configuration drift entirely.

As Anton continues his series on transitioning from raw requirements to immutable technical contracts, the underlying message remains clear: What is derived cannot drift, and what cannot drift requires nobody to remember it. In the complex landscape of modern distributed systems, eliminating the need to remember is the ultimate engineering achievement.

Did you find this story helpful?

Share it with your friends and colleagues on social media.

Share

Leave a Comment

Your email address will not be published. Required fields are marked *