← Back to Blog

Testing Distributed Systems

February 10, 2025

distributed systemstestingchaos engineeringmicroservices

Testing Distributed Systems: A Practical Guide

Testing distributed systems is hard. Multiple services, network failures, eventual consistency, and unpredictable failure modes make traditional testing approaches insufficient. Here's how to do it effectively.

The Challenge

In a distributed system, you're dealing with:

  • Services that communicate over unreliable networks
  • Data that's eventually (not immediately) consistent
  • Failures that cascade across services
  • Scenarios that are nearly impossible to reproduce

The good news? With the right strategies, you can build confidence in your system's reliability.

Core Testing Strategy

1. Unit Tests: Your Foundation (70%)

Focus: Business logic within individual services

Test each service in isolation with mocked dependencies. These should be:

  • Fast (milliseconds)
  • Deterministic (same input = same output)
  • Comprehensive (cover edge cases)

Example: Test your payment calculation logic without calling external payment gateways.

2. Integration Tests: Verify Connections (20%)

Focus: How services talk to each other

Use contract testing to ensure service interfaces don't break. Tools like Pact help you:

  • Define expected API contracts
  • Test one integration at a time
  • Catch breaking changes before deployment

Example: Test that your order service can actually call your inventory service with the expected payload format.

3. E2E Tests: Critical Paths Only (5%)

Focus: Complete user journeys that generate revenue

E2E tests in distributed systems are:

  • Slow (seconds to minutes)
  • Brittle (lots of moving parts)
  • Expensive to maintain

Only automate your most critical flows. Ask: "If this breaks, do we lose money or customers?"

Example: Complete checkout flow from cart to payment confirmation.

Chaos Engineering: Test What Breaks (5%)

The idea: If you never test failures, you don't know how your system handles them.

Start small:

  1. Kill a non-critical service during low traffic
  2. Introduce 100ms network latency
  3. Fill up disk space on a database
  4. Partition your network

Tools: Chaos Monkey, Gremlin, Pumba

What to verify:

  • Circuit breakers trip correctly
  • Fallbacks work as expected
  • The system degrades gracefully, not catastrophically
  • Error messages are meaningful

Real example: At Domino Data Lab, we regularly killed random service instances to ensure our MLOps platform could handle infrastructure failures without impacting users.

Non-Functional Testing

Performance Testing

Don't just test individual services — test the entire request path:

  • Where do requests slow down?
  • Do database connection pools get exhausted?
  • Do message queues back up under load?
  • Can the system handle 2x expected traffic?

Tools: Locust, k6, Gatling, JMeter

Observability Testing

Your logging and monitoring must work when things break.

Test these questions:

  • Can you trace a slow request across services?
  • Do your dashboards show the problem clearly?
  • Do alerts fire when they should?
  • Can you identify which service caused an issue?

If you can't answer these questions in your test environment, you definitely can't in production.

Consistency & Reliability

Eventual Consistency

If your system is eventually consistent, test that it actually becomes consistent:

  • How long does it take?
  • What happens if a service is down during synchronization?
  • Does data converge to the correct state?

Idempotency

Distributed systems retry operations. Your handlers must handle duplicate requests safely.

Test: Process the same message 2, 3, or 10 times. The result should be identical.

Example: Charging a credit card should only happen once, even if the request is retried.

Practical Environment Setup

Use Containers

Docker Compose or Kubernetes creates reproducible test environments with:

  • Multiple services
  • Databases and caches
  • Message queues
  • Network configurations

This makes integration tests reliable and repeatable.

Service Virtualization

Use tools like WireMock or Mountebank to simulate external dependencies:

  • Slow responses (500ms timeout)
  • Specific error codes (503, 429)
  • Edge cases hard to trigger in real APIs

The Testing Pyramid for Distributed Systems

      /\  5% - E2E Tests (critical user journeys only)
     /__\
    /    \ 5% - Chaos Engineering (ongoing experiments)
   /______\
  /         \ 20% - Integration Tests (contracts & connections)
 /___________\
/             \ 70% - Unit Tests (business logic in isolation)

Key Mindset Shift

Traditional approach: "Test every possible scenario"

Distributed systems approach: "Build systems that are observable, resilient by design, and can recover from unexpected states"

You can't test every possible failure mode in a distributed system. Instead:

  • Design for failure (circuit breakers, retries, timeouts)
  • Make your system observable (logs, metrics, traces)
  • Test that your resilience patterns actually work
  • Verify the system recovers gracefully

Common Mistakes to Avoid

Over-relying on E2E tests ✅ Use contract tests for service interactions

Testing in production only ✅ Chaos engineering in staging first, then production

Ignoring observability until things break ✅ Build logging and monitoring into your test strategy

Treating distributed systems like monoliths ✅ Accept that you can't control everything; test resilience instead

Bonus: Testing Circuit Breakers & Fallbacks

Circuit breakers and fallbacks are your safety net in distributed systems. Here's how to test them effectively.

What Are They?

Circuit Breaker = An electrical circuit breaker for service calls

  • Closed: Normal operation, requests flow through
  • Open: Too many failures, requests fail immediately without calling the service
  • Half-Open: Testing if the service recovered

Fallback = Your plan B when a service fails

  • Return cached data
  • Use default values
  • Call an alternative service
  • Degrade functionality gracefully

How to Test Circuit Breakers

1. Test state transitions:

# Verify circuit opens after threshold
for i in range(5):  # Threshold = 5 failures
    make_failing_request()

# Next request should fail fast (circuit open)
response = make_request()
assert response.circuit_open == True
assert actual_service_calls == 5  # Not 6!

2. Test recovery:

  • Circuit opens → wait for timeout → circuit goes half-open
  • Successful request → circuit closes
  • Failed request in half-open → circuit reopens

3. What to verify:

  • Circuit opens at the right threshold (e.g., 5 failures in 10 seconds)
  • Requests fail fast when circuit is open (don't wait for timeout)
  • Metrics are tracked correctly (circuit state, failure rate)
  • Thread-safe under concurrent requests

How to Test Fallbacks

1. Verify fallback executes when primary fails:

# Setup cache with fallback data
cache.set("user:123", cached_data)

# Simulate service failure
mock_service.return_error(503)

# Call should return cached data
response = get_user(123)
assert response.data == cached_data
assert response.from_fallback == True

2. Test fallback failure handling:

  • Primary fails → fallback executes → fallback also fails → proper error
  • Ensure you don't create infinite fallback chains

3. Verify data freshness:

  • Fallback data should have timestamps
  • Test behavior with stale vs. fresh cached data

Integration Testing Approach

Use tools like Toxiproxy or WireMock to inject failures:

Example test scenario:

  1. Deploy service with chaos proxy between dependencies
  2. Inject 500ms latency → verify timeout triggers fallback
  3. Inject 100% failure rate → verify circuit opens
  4. Remove failure → verify circuit recovers

What to validate:

  • Circuit state is exposed in metrics/health checks
  • SLOs are maintained when circuits open
  • No resource leaks (connections, threads) during failures

Key Metrics to Track

Your tests should verify these metrics exist:

  • Circuit state per dependency (open/closed/half-open)
  • Fallback execution rate
  • Request success vs. failure rate
  • Latency percentiles (P95, P99)
  • Time to recovery

Common Testing Mistakes

❌ Only testing happy paths ✅ Test recovery (circuit closing after success)

❌ Ignoring timeout configurations ✅ Ensure timeouts are coordinated across services

❌ Not testing concurrent requests ✅ Verify thread safety in circuit state management

❌ Forgetting to test the fallback itself ✅ Fallback should be fast and reliable

Conclusion

Testing distributed systems requires a different mindset. Focus on:

  • Unit tests for logic
  • Contract tests for service boundaries
  • Selective E2E tests for critical paths
  • Chaos engineering for resilience
  • Observability to understand failures

The goal isn't to prevent every failure—it's to ensure your system handles failures gracefully and recovers quickly.


What's your biggest challenge with testing distributed systems? Connect with me on LinkedIn to discuss.