Data Contracts and CI Enforcement

Concepts covered: Schema Evolution

Lesson 1's advanced tier introduced the pipeline-as-product framing: a pipeline has a contract that names producer, consumer, schema, freshness SLA, quality SLA, backfill policy, and deprecation policy. This section turns that framing into a working mechanism. A data contract is the executable form of the commitment. The producer commits to a shape and a set of guarantees; the consumer relies on them; the contract is checked in CI on every change so violations cannot ship. Without enforcement, contracts are documentation. With enforcement, they are the layer that prevents most of the silent failures the previous lessons spent so much effort detecting. The shift from documentation to enforcement is the central move. Documentation rots. CI does not. A contract that is a YAML file in a wiki page is fiction within six months because the producer evolves and the wiki does not. A contract that is a YAML file in the producer's repo, validated by a CI step, evolves at the speed the producer evolves and never disagrees with reality. Every team that has built a contract program at scale agrees that the enforcement step is the one that changes the failure rate; the document was always achievable.

What Makes A Contract A Contract

PropertyImplicit AgreementData Contract
FormTribal knowledge; Slack threads; an outdated wiki pageA versioned file checked into the producer's repo
EnforcementDiscovered after a consumer breaksChecked in CI on every producer change
VisibilityWhoever has been around long enough to knowDiscoverable by every consumer through a registry or catalog
VersioningImplicit; whatever was true the last time someone lookedSemver-style; breaking changes require a new major version
Failure modeProduction incidentFailed CI run; producer fixes before merge

Anatomy Of A Contract

# A producer-side contract for the customer events stream
contract:
  name: customer_events
  version: 2.3.0
  producer:
    team: platform-events
    repo: github.com/example/platform-events
    on_call: '#platform-events-oncall'
  schema:
    primary_key: event_id
    fields:
      - { name: event_id,        type: string,    nullable: false }
      - { name: customer_id,     type: string,    nullable: false }
      - { name: event_type,      type: string,    nullable: false,
          accepted_values: [signup, login, purchase, churn] }
      - { name: event_timestamp, type: timestamp, nullable: false }
      - { name: amount_usd,      type: decimal,   nullable: true,
          range: { min: 0, max: 1000000 } }
  guarantees:
    freshness: '<= 5 minutes p99'
    volume:
      daily_min: 1000000
      daily_max: 5000000
    uniqueness: event_id
    delivery: at_least_once
  consumers:
    - team: analytics-engineering
      use_case: leadership KPI dashboard
    - team: ml-platform
      use_case: churn prediction features
    - team: finance
      use_case: monthly billing aggregation
  evolution_policy:
    additive_changes: minor_version
    breaking_changes: major_version_with_90d_notice
    field_deletions: forbidden_in_minor

Where The Contract Is Enforced

StageWhat Is CheckedFailure Means
Producer pre-commitProducer code emits events that match the contract schemaPre-commit hook rejects the change
Producer CISchema, accepted values, ranges, primary key uniquenessPR cannot merge; producer fixes the violation
Pipeline ingestion gateReal events conform to the contract at the boundaryQuality gate halts; producer is paged
Consumer CIConsumer code reads the contract version it depends onConsumer build fails; consumer pins or upgrades
Registry validationContract version is registered and discoverableDeploy is blocked until registration succeeds

Producer and consumer bind through explicit version pinning. The producer publishes a contract version; the consumer pins to a version it has tested against. A consumer that depends on customer_events 2.3.0 declares that dependency in code and builds against it. When the producer publishes 2.4.0 (additive minor), the consumer's build still passes because 2.4.0 is backward-compatible with 2.3.0. When the producer wants to publish 3.0.0 (breaking), the contract evolution policy requires a 90-day notice and a major version bump; consumers receive notification and have time to migrate. This is exactly the pattern that semver versioning solved for software libraries; data contracts apply it to data.

# Consumer code declares its contract dependency explicitly
from contracts import customer_events

# Pin to a specific version; build fails if version is removed or unavailable
schema = customer_events.load(version='2.3.0')

for event in stream.read(schema=schema):
    # Type-checked against the pinned schema
    process_event(event)

What CI Enforcement Looks Like

# .github/workflows/contract-check.yml on the producer repo
name: contract-check
on: [pull_request]
jobs:
  validate-contract:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Validate contract syntax
        run: contract-cli validate contracts/customer_events.yaml
      - name: Detect breaking changes vs main
        run: contract-cli diff origin/main contracts/customer_events.yaml
      - name: Generate test fixtures from contract
        run: contract-cli fixtures contracts/customer_events.yaml > /tmp/fixtures.json
      - name: Run producer code against generated fixtures
        run: pytest tests/contract_compliance/

When Contracts Pay For Themselves

Pre-Contract World
  • Producer changes a column type; consumers break in production
  • Schema rolls forward without consumer awareness
  • Producers and consumers debate root cause during the incident
  • Adding a new consumer means archeology to find the actual schema
Post-Contract World
  • Breaking change is rejected at producer CI; never reaches consumers
  • Schema evolution follows semver; consumers see only versions they pin to
  • Root cause is named by the contract version; debate is short
  • New consumers read the contract registry and integrate without archeology

What Contracts Cannot Do Alone

Contracts catch shape violations and explicitly-named guarantee violations. They do not catch business-logic correctness on conforming data. A contract that says amount_usd is a decimal in the range zero to one million does not catch an amount that is the wrong number for the underlying transaction. The quality suite from the intermediate tier remains necessary. Contracts and quality suites are complements: contracts prevent shape failures, quality suites detect distribution-and-content failures. A serious data platform runs both.

Do
  • Version contracts with semver; additive minor, breaking major, fixed-bug patch
  • Enforce contracts in producer CI; production gates are the second line of defense
  • Publish contracts to a registry that consumers can discover programmatically
Avoid
  • Treat contracts as documentation; without enforcement they accumulate as fiction
  • Allow breaking changes in minor versions; consumers will silently break
  • Couple the contract format to one tool; contracts outlast any tool decision

About this lesson

This section is part of the Data Quality: Advanced lesson on DataDriven, the home of data engineering. Lessons cover SQL, Python, Spark, data modeling and pipeline architecture, and each section pairs its explanation with worked examples and practice you run as you go.