惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

云风的 BLOG
云风的 BLOG
雷峰网
雷峰网
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Cyberwarzone
Cyberwarzone
Hacker News: Ask HN
Hacker News: Ask HN
C
Cisco Blogs
NISL@THU
NISL@THU
C
Cyber Attacks, Cyber Crime and Cyber Security
L
LINUX DO - 热门话题
A
Arctic Wolf
Simon Willison's Weblog
Simon Willison's Weblog
S
Schneier on Security
P
Palo Alto Networks Blog
Know Your Adversary
Know Your Adversary
C
Cybersecurity and Infrastructure Security Agency CISA
G
GRAHAM CLULEY
K
Kaspersky official blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
V
Vulnerabilities – Threatpost
小众软件
小众软件
博客园 - 司徒正美
腾讯CDC
AWS News Blog
AWS News Blog
Last Week in AI
Last Week in AI
T
Tenable Blog
I
Intezer
博客园_首页
IT之家
IT之家
阮一峰的网络日志
阮一峰的网络日志
AI
AI
V
V2EX
Hacker News - Newest:
Hacker News - Newest: "LLM"
博客园 - 三生石上(FineUI控件)
W
WeLiveSecurity
D
Docker
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Security Latest
Security Latest
F
Fortinet All Blogs
S
Secure Thoughts
T
Troy Hunt's Blog
T
The Blog of Author Tim Ferriss
Recorded Future
Recorded Future
M
MIT News - Artificial intelligence
GbyAI
GbyAI
Microsoft Security Blog
Microsoft Security Blog
L
LINUX DO - 最新话题
B
Blog RSS Feed
U
Unit 42
TaoSecurity Blog
TaoSecurity Blog

The Practical Developer

The Libuv Thread Pool Trap: Why Node.js Async APIs Stall Under Load Postgres Covering Indexes with INCLUDE: Eliminate Heap Fetches on Read-Heavy Workloads Postgres DISTINCT ON: The Fastest Way to Get the Latest Row Per Group Postgres Transaction Isolation: The Anomalies Your App Actually Faces in Production Linux TCP Tuning for Node.js Microservices: The Kernel Settings That Stop Silent Connection Drops Under Load Postgres HOT Updates and Fillfactor: Why Not All Writes Are Created Equal Database Connection Pool Leaks: Finding the Promise That Never Returns Its Seat Linux OOM Killer in Production: Why Your Node.js Containers Die Without a Stack Trace Postgres Materialized Views: Refresh Strategies That Do Not Lock Your Dashboards API Dependency Health Checks: Why /health Is Not Enough Authorization with Zanzibar Tuples: How Google Manages Permissions and How To Build the Same Check in Node.js Postgres Advisory Locks: The 20-Character Primitive That Replaces Redis for Coordination Dead Letter Queues: The Message Queue Pattern That Saves You at 2 a.m. File Descriptor Exhaustion: The Kernel Limit That Silently Drops Node.js Connections Graceful Degradation: The Pattern That Turns Total Outages into Partial Success PostgreSQL Full-Text Search: Dropping Elasticsearch for 90% of Use Cases S3 Presigned Multipart Uploads: Stop Your API Server from Being a File Upload Bottleneck MessagePack vs JSON: The Binary Serialization Switch That Cut Our Internal RPC Overhead by 40% DNS Caching in Node.js: The Silent Cause of Production Latency Spikes Reliable Cron Jobs: The Pattern That Stops Double Runs, Missed Executions, And The 2 AM Page GraphQL Query Complexity: Stop the OOM Query Before It Reaches Your Resolver Node.js Event Loop Lag: The Hidden Metric Behind Random Latency Spikes API Request Validation with Zod: The Schema That Catches Bad Input Before It Corrupts Your Database Load Shedding in Node.js: How to Reject Traffic Before You Drown Request Hedging: Cut Tail Latency In Half Without Overprovisioning Git Bisect: The Automated Binary Search That Finds Breaking Commits in Minutes Node.js Garbage Collection Tuning: Stop Letting V8 Pause Your Event Loop Node.js Server Timeouts: The Settings That Stop Slow Clients from Holding Sockets Hostage Postgres BRIN Indexes: The Time-Series Secret That Shrinks Indexes by 99% Event Sourcing with PostgreSQL: The Pragmatic 80% Solution Node.js Cluster Mode: Scaling the Event Loop Across CPU Cores Postgres Partial Indexes: Stopping Soft Deletes from Ruining Your Query Performance Request Coalescing with the Singleflight Pattern: Stop Drowning Your Database on Every Cache Miss The Bulkhead Pattern: Why One Slow Endpoint Should Not Drown Your Whole Service Node.js AsyncLocalStorage: End-to-End Request Context Without the Propagation Hell Postgres Deadlocks: Logging the Victim, Reproducing the Race, and Fixing the Lock Order Your Node.js HTTP Client Is the Bottleneck: Connection Pool Tuning That Works Optimistic Locking in Postgres: Stop Losing Data to Race Conditions Postgres Read Replicas: Stop Serving Stale Data to Your Users Cursor Pagination: Why Offset Queries Explode at Scale and How to Fix Them Node.js Worker Threads: 60 Lines That Stop a CSV Upload from Timing Out Every Other Request Reliable Webhook Delivery: Architecture for Outbound HTTP You Can Trust Request Timeouts and Deadline Propagation: Stop the Chain of Slowness Advanced Security Practices in Node.js Graceful Shutdown in Node.js: The 40 Lines That Stop 502s During Deploys Finding Node.js Memory Leaks with Heap Snapshots Idempotency Keys in 30 Lines: Stop Your Webhook From Charging Customers Twice Backpressure In Node.js: The Fix For Slow-Motion Queue Meltdowns Retries Done Right: Jitter, Budgets, and the Stampede You Did Not See Coming The Cache Stampede: Why Your "Just Add Redis" Layer Crashes Postgres at 3 a.m. Postgres SKIP LOCKED: An 80-Line Job Queue You Can Run Without Redis Stop Doing Work Nobody Wants: AbortController in Node.js, Done Right The N+1 Query Problem: We Found 23 In One Codebase And Killed Every One I Tried 5 AI Coding Tools for a Month. Here Is What I Actually Use CI/CD From Zero to Production in 30 Minutes With GitHub Actions Node.js vs Bun vs Deno: Which Runtime Should You Pick in 2025? Kubernetes Resource Requests And Limits: The Numbers That Decide If Your Cluster Is Stable The Three Pillars of Observability Are A Myth: What Actually Matters In Production pnpm Vs npm Vs yarn Vs Bun For Monorepos: Which One Earns The Migration In 2024 JSONB Indexing In Postgres: GIN Vs Expression Indexes, And When Each Is The Right Choice A Code Review Checklist That Ends The Same Three Arguments Every Sprint gRPC Vs REST In 2024: When The Switch Pays For Itself React Suspense For Data Fetching: The Pattern That Replaces Half Your Loading State Code The Five-Stage Rollout: How To Ship A Risky Change Without Holding Your Breath GitHub Actions In A Monorepo: Caching, Path Filters, And Secret Boundaries That Actually Work The Blameless Postmortem That Actually Improves Things: A Template And Six Hard-Won Rules Recursive CTEs In Postgres: How To Query A Tree Without N Round Trips Node.js Streams: When They Actually Help, And When They Just Add Complexity Playwright Vs Cypress In 2024: The Honest Comparison Of Which One Earns The Test Time React Server Components: The Mental Model That Makes The "use client" Boundary Obvious Pod Disruption Budgets: The K8s Object That Keeps Your Service Up During Cluster Maintenance Postgres LISTEN/NOTIFY: The Pub/Sub You Already Have And Are Not Using Chaos Engineering Starter Kit: The Five Drills That Don't Need Netflix-Scale Spec-Driven API Development With OpenAPI: How To Stop Drifting From Your Docs Kubernetes Autoscaling Beyond CPU: The Custom-Metric HPA Pattern That Actually Works Postgres Partitioning For Time-Series: The Boring Setup That Saves Your Database Distributed Locks With Redis: An Honest Look At Redlock And When You Don't Need It HTTP/2 vs HTTP/3: What Actually Changes For Your App, And What Doesn't Image Optimization For The Web In 2023: srcset, AVIF, And The Lighthouse Score You Actually Want Kafka vs RabbitMQ: A Decision Tree That Doesn't Hate You UUID vs Bigint Primary Keys In Postgres: The Index Math That Decides For You Flame Graphs: How To Find The Slow Function In 30 Seconds Without Profiling Theatre Postgres Streaming Vs. Logical Replication: Which One Solves Your Actual Problem ESLint Rules That Earn Their Keep: The Twelve I Enable On Every Project Pre-Commit Hooks That Pay For Themselves: Husky, lint-staged, And The Five Rules That Stick Zero-Downtime Database Migrations: The Six-Step Pattern That Rules Them All Circuit Breakers In Node.js: 50 Lines That Stop A Failing Dependency From Taking Down Your Service Postgres VACUUM Is Not Magic: How Your Hot Table Bloats To 80GB And How To Fix It Kubernetes Liveness And Readiness Probes: The Difference That Causes Half Your Outages Rate Limiting In Production: A Token Bucket In 30 Lines Of Redis The Outbox Pattern: How To Stop Losing Events When Postgres And Kafka Disagree Load Testing With k6: The Three Scenarios That Find Real Bugs (Not Synthetic Numbers) Postgres Row-Level Security For Multi-Tenant Apps: The Pattern That Stops You From Leaking Data Rebase vs. Merge: The Team Policy That Ends The Argument Forever OpenTelemetry in Node.js: Distributed Tracing That Actually Helps During an Incident Feature Flags That Pay Rent: The 4 Flag Types And When To Delete Each ETag, Last-Modified, and the Caching Headers Most APIs Get Wrong Connection Pooling Without the Cargo Cult: pgbouncer in 100 Lines of Config JSONB Is Not a Schema: When To Reach For It in Postgres, And When To Stop Bash Strict Mode: The Three Lines That Stop Your Deploy Script From Lying To You
Contract Testing Stops Microservice Integration Failures Before Deploy
The Practica · 2026-06-14 · via The Practical Developer

Your team owns the checkout service. Another team owns the inventory service. Your service calls theirs to reserve stock before charging the customer. The integration tests pass in staging. The deployment pipeline is green. You ship.

Three minutes later, PagerDuty lights up. Every checkout is failing with a 500 error. The inventory team deployed a new version of their API that renamed stockLevel to availableQuantity and swapped the response format from an object to an array. They updated their own consumers. They did not know yours existed. Their tests passed. Yours passed. The integration broke.

This is not a blame problem. It is a contract problem. Two services agreed on a shared interface implicitly, through a README and a Slack message from three months ago. When one side changed the interface without the other knowing, there was no mechanism to catch the mismatch before production.

Consumer-driven contract testing (CDCT) is the mechanism. It formalizes the implicit agreement between a service consumer and a service provider into a machine-readable contract that both sides verify independently. This post walks through the pattern with Pact, the most mature CDCT tool for Node.js, from zero to a running pipeline.

What contract testing fixes

Integration tests test that two real services work together. They are expensive to write, slow to run, and brittle. They require both services to be deployed, a database to be seeded, and network calls to work. They also test too much: they verify that the entire stack works end-to-end when what you really need to know is whether your API client still matches the provider’s response format.

End-to-end tests catch contract breaks, but they do it late (after deploy to a shared environment) and noisily (a timeout in the database layer fails the same test that was supposed to verify the response shape). When the suite takes fifteen minutes to run and flakes twice a week, teams stop trusting it.

Unit tests on the consumer side mock the HTTP response. They verify your deserialization logic works, but the mock describes what you think the provider returns, not what it actually returns. When the provider changes the response, your mock stays stale and your tests stay green. The mismatch is discovered in production.

Contract testing sits between these two extremes. The consumer publishes a contract (an “I expect this request to return this shape” declaration). The provider verifies that declaration against its real API. Both sides stay independent. No shared deployment, no database, no network flakiness. The contract is the only shared artifact.

The Pact workflow in three steps

Pact is a consumer-driven contract testing framework. The flow is:

  1. Consumer writes a Pact test that describes the request it will make and the response it expects. Running the test generates a pact file (a JSON contract).
  2. Consumer publishes the pact file to a Pact Broker (a shared repository).
  3. Provider runs a verification suite that replays the consumer’s expected requests against its real API and checks that the responses match.

If the verification passes, both services are compatible. If it fails, the provider knows exactly which consumer will break and what the mismatch is, before anyone deploys.

Step 1: Consumer writes the pact

You are the checkout service. You call the inventory service to reserve items. Here is the Pact test that describes that interaction.

First, install the Pact library:

npm install --save-dev @pact-foundation/pact

Then write the consumer-side Pact test.

// checkout/test/contract/inventory.pact.test.ts
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
import { InventoryClient } from '../src/inventory-client';

const { like, eachLike, integer, string } = MatchersV3;

const provider = new PactV3({
  consumer: 'checkout-service',
  provider: 'inventory-service',
  dir: path.resolve(__dirname, '../../pacts'),
});

describe('Inventory API contract', () => {
  beforeAll(() => provider.setup());
  afterAll(() => provider.finalize());

  describe('POST /reserve', () => {
    it('reserves stock and returns a confirmation', async () => {
      // Arrange: tell Pact what request we expect and what response to return
      await provider.addInteraction({
        uponReceiving: 'a request to reserve stock',
        withRequest: {
          method: 'POST',
          path: '/reserve',
          headers: { 'Content-Type': 'application/json' },
          body: {
            sku: 'SKU-001',
            quantity: 2,
            requestId: string('req-abc-123'),
          },
        },
        willRespondWith: {
          status: 200,
          headers: { 'Content-Type': 'application/json' },
          body: like({
            reservationId: string('rsv-xyz-789'),
            sku: string('SKU-001'),
            quantityReserved: integer(2),
            expiresAt: string('2026-06-14T10:00:00Z'),
          }),
        },
      });

      // Act: use the real client against Pact's mock server
      const client = new InventoryClient(provider.mockService.baseUrl);
      const result = await client.reserveStock('SKU-001', 2, 'req-abc-123');

      // Assert: the client parsed the response correctly
      expect(result.reservationId).toBeTruthy();
      expect(result.quantityReserved).toBe(2);
    });
  });
});

The key detail is the matchers (like, eachLike, integer, string). These tell Pact which parts of the response shape must match exactly and which parts are variable. For example, string('req-abc-123') means “a string is expected, but the exact value can differ when the provider verifies.” This is what makes Pact tests flexible: they do not lock in exact values, only the shape and type.

Run the test:

npx jest checkout/test/contract/inventory.pact.test.ts

This generates a JSON pact file at pacts/checkout-service-inventory-service.json.

{
  "consumer": { "name": "checkout-service" },
  "provider": { "name": "inventory-service" },
  "interactions": [{
    "description": "a request to reserve stock",
    "request": {
      "method": "POST",
      "path": "/reserve",
      "headers": { "Content-Type": "application/json" },
      "body": {
        "sku": "SKU-001",
        "quantity": 2,
        "requestId": "req-abc-123"
      }
    },
    "response": {
      "status": 200,
      "headers": { "Content-Type": "application/json" },
      "body": {
        "reservationId": "rsv-xyz-789",
        "sku": "SKU-001",
        "quantityReserved": 2,
        "expiresAt": "2026-06-14T10:00:00Z"
      }
    }
  }]
}

This JSON is the contract. It says: “The checkout service sends a POST with these fields and expects this response shape.” Commit this file. Publish it to the Pact Broker.

Step 2: Publish the pact

Set up a Pact Broker (they offer a free developer tier at pactflow.io, or you can run one with Docker):

docker run -d -p 9292:9292 \
  -e PACT_BROKER_DATABASE_URL=postgres://postgres@postgres/postgres \
  pactfoundation/pact-broker

Publish the pact from your CI pipeline:

npx pact-broker publish ./pacts \
  --broker-base-url https://your-broker.example.com \
  --consumer-app-version $GIT_COMMIT \
  --branch $GIT_BRANCH

The Pact Broker stores the contract and shows a matrix of which consumer versions are compatible with which provider versions. When the provider runs its verification, the broker tells it exactly which pacts to verify.

Step 3: Provider verifies the pact

Now the inventory service team takes over. They add a verification step to their test suite.

// inventory/test/contract/verify-pacts.ts
import { Verifier } from '@pact-foundation/pact';
import { startServer } from '../src/server';

describe('Pact verification', () => {
  let server;

  beforeAll(async () => {
    server = await startServer(4000);
  });

  afterAll(async () => {
    await server.close();
  });

  it('satisfies all consumer pacts', async () => {
    const opts = {
      provider: 'inventory-service',
      providerBaseUrl: 'http://localhost:4000',
      pactBrokerUrl: 'https://your-broker.example.com',
      pactBrokerToken: process.env.PACT_BROKER_TOKEN,
      publishVerificationResult: true,
      providerVersion: process.env.GIT_COMMIT,
      consumerVersionSelectors: [
        { mainBranch: true },
        { deployedOrReleased: true },
      ],
    };

    await new Verifier(opts).verifyProvider();
  });
});

Run the verification:

npx jest inventory/test/contract/verify-pacts.ts

The Verifier fetches every pact that applies to the inventory service from the broker. For each interaction, it sends the exact request described in the pact to the real running provider and checks that the response matches the expected shape (allowing for flexible matchers).

If the checkout service expects a reservationId string and the provider now returns a number, the verification fails with a clear diff:

Verification failed for interaction "a request to reserve stock"

Expected response body:
  reservationId: String

Actual:
  reservationId: 12345

Mismatch: Expected reservationId to be a String but got Integer

No deploy needed. No staging environment required. The provider knows their change will break the checkout service before they merge the PR.

Setting up the CI pipeline

The contract testing flow only works if it is automated. Here is the minimal CI setup for each side.

Consumer CI (checkout-service):

# .github/workflows/consumer-contracts.yml
name: Consumer contract tests
on: [push]
jobs:
  test-and-publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npm ci
      - run: npm test -- --testPathPattern=contract

      - name: Publish pacts
        run: |
          npx pact-broker publish ./pacts \
            --broker-base-url ${{ secrets.PACT_BROKER_URL }} \
            --consumer-app-version ${{ github.sha }} \
            --branch ${{ github.ref_name }}
        if: success()

Provider CI (inventory-service):

# .github/workflows/provider-contracts.yml
name: Provider contract verification
on: [push]
jobs:
  verify:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: test
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npm ci

      # Provider verification against a real, running server
      - run: |
          npx jest --testPathPattern=verify-pacts \
            --forceExit
        env:
          PACT_BROKER_URL: ${{ secrets.PACT_BROKER_URL }}
          PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}
          DATABASE_URL: postgres://postgres:test@localhost:5432/postgres

The provider publishes its verification results back to the broker. The broker then marks the contract as verified (green checkmark) or failed (red X). The consumer’s CI can read this status and block merges if the contract is not satisfied.

What to test and what to skip

Not every API interaction needs a contract test. Here is the heuristic I use.

Test with a contract when:

  • The consumer and provider are owned by different teams or deployed independently
  • The API response shape is non-trivial (nested objects, arrays, conditional fields)
  • The consumer is in production and any regression is immediately visible

Skip contract tests when:

  • Both sides are in the same deployable unit (same repo, same deploy)
  • The API is internal to a single team and both sides are always deployed together
  • The endpoint is experimental and changes daily

Contract tests replace a subset of integration tests. They do not replace end-to-end tests for critical user journeys. They do not replace unit tests for business logic. They replace the specific failure mode where two independently deployed services disagree on the shape of a response.

Handling breaking changes the right way

When a contract verification fails, the provider cannot simply change the response and merge. They need to follow a versioning strategy.

The cleanest approach for REST APIs is to add a new field or endpoint without removing the old one, then coordinate with the consumer to migrate. Pact makes this explicit: the broker shows exactly which consumers depend on which fields. If you want to remove stockLevel, the broker tells you that checkout-service still uses it. You cannot remove it until checkout-service publishes a new pact that no longer references it.

For versioned APIs (e.g., /v1/reserve and /v2/reserve), use separate pacts for each version. The consumer publishes against the version it targets, and the provider verifies both versions independently.

The Pact Broker supports a “can I deploy” check that queries the matrix:

npx pact-broker can-i-deploy \
  --pacticipant inventory-service \
  --version $GIT_COMMIT \
  --to-environment production

This returns a boolean and an exit code. If any consumer that depends on inventory-service is not verified against this version, the check fails. Wire this into your deploy pipeline as a gating step. If the check fails, the deploy is blocked.

The maturity curve

Teams usually go through three phases with contract testing.

Phase 1: One consumer, one provider. The first team writes a pact, publishes it manually, and the provider runs verification on their machine. This already catches the most common contract breaks and takes an afternoon to set up.

Phase 2: CI automation. The consumer publishes pacts on every push. The provider verifies in CI and publishes results. The broker becomes the source of truth for API compatibility. Both teams can see the verification status in their pull requests.

Phase 3: Deploy gating. The “can I deploy” check runs in the provider’s CD pipeline. If the check fails, the deploy is blocked. The consumer’s CD pipeline also checks that their pacts are verified against the provider version running in production. This eliminates the deploy-time surprise entirely.

Phase 1 takes a day. Phase 2 takes a week. Phase 3 takes buy-in from your release engineering team but is the most impactful investment you can make for microservice reliability.

Contract testing assumes the provider and consumer communicate synchronously (HTTP request/response). If your services communicate through a message broker (Kafka, RabbitMQ) or an event stream, Pact has experimental support for asynchronous interactions, but the patterns are less mature. For async contracts, consider schema registries (like Confluent Schema Registry for Avro) or AsyncAPI with a contract testing tool like Microcks.

Contract testing also assumes you control both sides of the communication. If you are consuming a third-party API, you cannot make the provider verify your pact. In that case, use Pact to generate the mock client and test your consumer, but skip the provider verification step. The pact file is still useful documentation of how you expect the external API to behave.

The takeaway

The checkout service and the inventory service were never going to catch their contract break with unit tests (mocks are stale) or end-to-end tests (too slow, too flaky). The mismatch was invisible until production because neither side had a formal description of what they agreed on.

Consumer-driven contract testing with Pact gives you that formal description. It runs in unit-test time (milliseconds, not minutes). It runs independently on each service. It catches the exact failure mode of “someone renamed a field and deployed without telling the other team.” And it creates an audit trail of who depends on what, so breaking changes are negotiated instead of discovered at 3 p.m. by PagerDuty.

Publish your first pact this week. It takes an afternoon, and it is the single highest-leverage reliability investment for a microservice architecture.


A note from Yojji

Building microservice architectures where every team can deploy independently without breaking the rest of the system requires more than good intentions. It requires the kind of disciplined testing strategy, contract negotiation, and CI/CD maturity that comes from shipping production systems day in and day out. Yojji builds and deploys Node.js microservices for clients who cannot afford integration surprises.

Yojji is an international custom software development company with offices in Europe, the US, and the UK. Their teams work extensively with the JavaScript ecosystem (React, Node.js, TypeScript), cloud platforms (AWS, Azure, Google Cloud), and distributed system patterns. If your team is scaling microservices and wants to ship faster without breaking things, Yojji is worth a conversation.