Picture a mid-size e-commerce company in 2018. Their platform is a single Rails app: user accounts, product catalogue, shopping cart, order management, and payment processing all live in one codebase, deploy together, and share one PostgreSQL database. It works — until it doesn't. A Black Friday traffic spike takes down the whole thing because the recommendation engine is hammering the same DB. A broken migration in the payments module prevents the entire app from deploying. The team decides to split the monolith.
Two years later they have eight services: user-service, product-service, cart-service, order-service, payment-service, notification-service, warehouse-service, and search-service. Each deploys independently. Each owns its own database. Life is better — until the QA engineer sits down to write integration tests.
What microservices actually are
A microservice is a small, independently deployable unit of software that owns a single business capability. "Small" is deliberately vague — the useful definition is independently deployable: the team responsible for payment-service can release a new version on a Tuesday afternoon without coordination with the order-service team.
Three properties define the architecture:
- Own data. Each service has its own database (or schema).
order-servicecannot directly querypayment-service's tables. It must ask via API. - Own deployment cycle. Services are built, versioned, and released separately.
product-servicecan be on v4.2 whilecart-serviceis on v1.8. - Communication via network. Services talk over HTTP/REST, gRPC, or asynchronous message queues (Kafka, RabbitMQ). There are no in-process method calls between services.
The split in practice
The monolith had one codebase and one database. The microservices version looks like this:
order-servicereceives a new order, writes to its ownorders_db, then publishes anorder.createdevent to Kafka.payment-serviceconsumes that event, charges the card, writes topayments_db, and publishespayment.completed.warehouse-serviceconsumespayment.completed, reserves inventory, and publishesinventory.reserved.notification-serviceconsumes both events and sends confirmation emails.
Each service is simpler than the monolith was. The system is more complex.
Why testing is fundamentally different
Testing a monolith is mostly a solved problem. You write unit tests, spin up the app with a test database, and run integration tests. One process. One DB to query. One deployment to manage.
Microservices break almost every assumption that makes that easy.
1. Distributed state — no single DB to query
In the monolith you verified an order was placed with one SQL query. In microservices, "order placed successfully" means: order-service has a row, payment-service has a transaction, warehouse-service has a reservation, and notification-service sent an email. Verifying that in a test means making four API calls (or querying four databases), then asserting all four are consistent. If any one of them is slow or temporarily inconsistent, your test has a problem.
2. Network failures must be tested explicitly
In-process method calls do not fail unless the code throws. Network calls fail all the time — the connection times out, the downstream service returns a 503, a response arrives half-complete. Your order-service tests need to cover: what happens when payment-service is unavailable? What happens when it takes 10 seconds to respond? Does order-service retry? Does it circuit-break? Does it leak threads? None of these questions exist in a monolith.
3. Independent deployments silently break contracts
The order-service team ships v2 and changes the POST /orders response body — a field renamed from orderId to order_id. They tested v2 in isolation. It passes. But notification-service v1 is still reading orderId and now silently drops every new confirmation email. Neither service's own test suite caught this. The only test that would have caught it is a cross-service contract test — a test type that does not exist in a monolith.
4. Test environment complexity
Running an integration test for the checkout flow means spinning up order-service, payment-service, warehouse-service, notification-service, their four databases, and the Kafka broker they communicate through. That is a non-trivial Docker Compose file. It is slow to start, expensive to run in CI, and fragile when one container fails to become healthy in time. In the monolith, the equivalent test required one app and one database.
5. Eventual consistency
When a user clicks "Place Order," the response comes back immediately from order-service. But payment-service has not processed the payment yet — it will, once it consumes the Kafka event, usually within a few hundred milliseconds. The "order confirmed" page is shown to the user before the payment is actually confirmed. Your test cannot assert on payment state immediately after placing an order. It must wait — and deciding how long to wait, and what to do if it times out, is a real design problem in test code.
6. Cascading failures
warehouse-service slows down due to a bad database query. order-service calls warehouse-service for every order, waits for a response, holds open an HTTP connection. After 30 seconds, all 200 of order-service's connection pool threads are waiting on warehouse-service. order-service is now effectively down — and it never had a bug. Testing resilience patterns (timeouts, bulkheads, circuit breakers) requires deliberately making services fail in controlled ways. Chaos engineering at the test level.
The honest trade-off
Microservices are not a free upgrade from monoliths. They trade deployment independence for testing complexity, and operational simplicity for organizational scalability. Both sides of that trade are real.
A monolith is easier to test thoroughly. A microservices system scales deployment ownership across many teams — but the test bill comes due in complexity. This course is about paying that bill efficiently.
If you need to brush up on HTTP fundamentals — methods, status codes, headers, request/response structure — the API Testing Masterclass covers that ground in depth. This course assumes you are comfortable with HTTP and focuses on the distributed-systems complications layered on top.
Monolith vs. microservices at a glance
⚠️ Common mistakes
- Testing microservices like a monolith. The most common failure: writing a full end-to-end test for every scenario that calls all 10 services, waiting for it to stabilise, and discovering it is too slow and fragile to run reliably. Microservices require a different test strategy — not just more tests.
- Ignoring network failure modes. Teams stub external services with happy-path responses and never test what happens on timeout, 503, or a malformed response. Those failures are not edge cases in production; they are everyday events at scale.
- Assuming eventual consistency is instant. Writing assertions immediately after triggering an asynchronous workflow and not building in polling or retry logic produces flaky tests that fail under load or in slower CI environments.
🎯 Practice task
Work through this scenario with pen and paper or a whiteboard — no code required.
- Take the e-commerce checkout flow described in this lesson (
order-service→ Kafka →payment-service→warehouse-service→notification-service). Draw the call graph: which services communicate with which, and how (HTTP or event). - For each communication link in your diagram, write down one failure mode — what happens if that link fails? (e.g., "payment-service returns 503 mid-checkout").
- For the full "user places an order" workflow, list every piece of state you would need to verify to be confident the test passed. Which service owns each piece of state?
- Estimate how many separate HTTP calls or database queries a complete end-to-end test would need to make to verify all state from step 3. Note whether that number surprises you.
- Identify which of the six testing challenges from this lesson (distributed state, network failures, independent deployments, environment complexity, eventual consistency, cascading failures) you think would be hardest to handle in your current project or a project you have worked on. Write two sentences on why.
The next lesson introduces the microservices testing pyramid — a framework for deciding where each type of test belongs so you get confidence without the overhead of spinning up all 10 services for every check.