QA Interview Hub · Knowledge base · Quizzes
System Design
Microservices and test strategy
A microservices testing strategy
In a monolith an e2e test covers everything at once. In microservices there are 30 services, each team deploys independently, and a full e2e on every change becomes slow and unstable. So the strategy is different.
e2e (few, critical paths)
contract tests (between services)
component tests (a service in isolation, dependencies are stubs)
integration (a service + its DB, queue)
unit (many)
A component test tests one service through its API, with a real DB in a container (Testcontainers), while neighbouring services are replaced by stubs. Fast, stable, runs in the service's CI.
A contract test checks that a producer and a consumer understand the API the same way. The consumer-driven approach (Pact): the consumer describes which requests it makes and which responses it expects, and the producer in its own CI verifies that it fulfils these contracts. That way a breaking API change is caught before deployment, without a shared environment.
E2E is left for a few critical business flows: registration, top-up, transfer, withdrawal. There must not be hundreds of them.
What else is important:
- Testing asynchronous contracts: the format of events in a queue is also a contract.
- Backward compatibility: a new service must accept the old format until all consumers have updated.
- Each team owns its service's tests. The QA architect sets standards and shared tools.
In an interview: For microservices I shift the main weight to component and contract tests, which run in each service's CI without a shared environment. I leave E2E only for a few critical money flows, because hundreds of e2e tests through 30 services would be slow and unstable.
Task: how to test a transfer system with a queue
The condition: a user creates a transfer through the API. Transfer Service writes the transfer with the status PENDING and publishes an event to the queue. Ledger Service debits and credits the funds, then publishes an event, and the status becomes COMPLETED. Notification Service sends a push.
Client -> Transfer API -> DB (PENDING) -> Queue -> Ledger -> Queue -> Transfer (COMPLETED)
\-> Notification
Requirements and risks
Money must not be debited twice or lost. A balance doesn't go negative. Every transfer reaches a final status: COMPLETED or FAILED. A repeated client request doesn't create a second transfer.
Test layers
- Unit: fee calculation, amount validation, status transitions.
- Component Transfer Service: the API plus a real DB, the queue as a stub or a container. We check that the event is published in the right format.
- Component Ledger: we feed an event into the queue and check the ledger records. A duplicate event gives one debit.
- Contract: the event format between Transfer and Ledger.
- E2E: create a transfer through the API, wait for
COMPLETED with expect.poll, check both balances.
Negative scenarios
- Insufficient funds: status
FAILED, the balance didn't change. - A repeated POST with the same idempotency key: the same transfer.
- Ledger failed mid-processing: after a restart the transfer completes once.
- An invalid event goes to the DLQ, and there is an alert.
- 20 parallel transfers from one account for an amount greater than the balance: none gives a minus.
Metrics
The number of transfers "stuck" in PENDING longer than N minutes, DLQ sizes, consumer lag, discrepancies in balance reconciliation.
In an interview: I break the system into layers: unit for logic, component for each service with a real DB, contract for the event format and a few e2e tests that wait for the final status. The main focus is negative scenarios: duplicates, a consumer failing, parallel debits and transfers stuck in PENDING.
Eventual consistency and sagas in tests
Eventual consistency means that after a change, data in different parts of the system becomes consistent not at once but after some time. The balance in Ledger has already changed, and the history service hasn't yet.
How to test:
- Don't check immediately after the action. Wait for the final state with polling and a timeout:
expect.poll or expect(...).toPass(). - The timeout must match the SLA. If the business promises an update within 5 seconds, a test with a 60-second timeout will miss a degradation.
- No fixed
sleep: it is either too short (flaky) or too long (slow).
A saga is a way to perform a business operation across several services without a distributed transaction. Each step has a compensating action. For example an e-commerce order: reserve the item, debit the payment, create the delivery. If creating the delivery failed, refund the money and release the reservation.
What to test in a saga:
- A failure at each step and the right compensation.
- A failure of the compensation itself: is there a retry and an alert.
- Repeated delivery of events at each step.
- The final state: there are no "half-finished" operations where money is debited and the item isn't reserved.
await expect(async () => {
const order = await api.getOrder(orderId);
expect(order.status).toBe('CANCELLED');
expect(order.refundStatus).toBe('REFUNDED');
}).toPass({ timeout: 10_000 });
In an interview: In an asynchronous system I check the final state through polling with a timeout that matches the SLA, not through sleep. For sagas I test a failure at each step and that the compensation returns the system to a consistent state, for example the money is refunded if the order is cancelled.