EPAM SystemsSenior QA Automation Engineer2023-2024

Microservices Contract Testing

Implementing consumer-driven contract testing to eliminate integration failures in a complex microservices architecture with 20+ services.

Pact.jsPactBrokerJestNode.jsGitLab CI/CDDockerOpenAPI
0
Production Incidents (12+ months)

The Challenge

The microservices ecosystem had grown to 20+ services, each owned by different teams deploying independently. API compatibility issues were causing 3-4 production incidents per month, with each incident requiring emergency rollbacks and costing an average of $50K in downtime and engineering time.

  • 20+ microservices with complex interdependencies
  • 3-4 production incidents monthly due to API breaking changes
  • Integration tests taking 2+ hours and frequently flaky
  • No visibility into API compatibility until deployment to staging
  • Teams deploying blindly without knowing impact on consumers

The Solution

  • Implemented Pact.js for consumer-driven contract testing across all services
  • Deployed PactBroker as central contract registry with version management
  • Created 'can-i-deploy' checks integrated into GitLab CI pipelines
  • Built automated contract generation from OpenAPI specs
  • Established contract versioning strategy aligned with semantic versioning

We adopted a consumer-driven approach where frontend teams define their API expectations as contracts. These contracts are published to PactBroker, and backend services verify against them before deployment. The 'can-i-deploy' CLI tool prevents any service from deploying if it would break existing contracts.

Consumer Contract Definition

typescript
// Consumer Pact test for User Service
import { PactV3, MatchersV3 } from '@pact-foundation/pact';

const provider = new PactV3({
  consumer: 'WebApp',
  provider: 'UserService',
});

describe('User API Contract', () => {
  it('returns user profile', async () => {
    await provider
      .given('user exists', { userId: '123' })
      .uponReceiving('a request for user profile')
      .withRequest({
        method: 'GET',
        path: '/api/users/123',
        headers: { Authorization: like('Bearer token') }
      })
      .willRespondWith({
        status: 200,
        body: {
          id: string('123'),
          email: email(),
          createdAt: iso8601DateTime(),
        }
      });

    await provider.executeTest(async (mockServer) => {
      const response = await userClient.getProfile('123');
      expect(response.id).toBe('123');
    });
  });
});

Key Results

Quantifiable outcomes achieved through this implementation

0
API Incidents
100%
Deploy Confidence
-85%
Integration Time
12+
Months Stability

Performance Metrics

Contract Testing Flow

1
Consumer Defines Contract
Frontend team writes Pact tests specifying expected API behavior and response schemas
2
Publish to PactBroker
Contracts are versioned and stored centrally, tagged with consumer version and environment
3
Provider Verification
Backend services verify their implementation against all consumer contracts automatically
4
Can-I-Deploy Check
CI pipeline queries PactBroker to ensure deployment won't break any consumers