Welcome back. In our last lesson, we witnessed the culmination of all our work by performing a successful end-to-end test of our e-commerce platform. We saw how a single HTTP request gracefully flowed through our API Gateway, triggered a synchronous gRPC call, and kicked off an asynchronous, event-driven workflow. Our system works perfectly—as long as all its parts are running perfectly.
This final lesson of the course tackles a more realistic and crucial question: what happens when things go wrong? We will step into the world of resilience testing and Chaos Engineering, the discipline of intentionally injecting failures into a system to build confidence in its ability to withstand them. Your goal is to simulate a service failure in our running application and observe, firsthand, how our architecture responds to the disruption.
1. Why Break Things on Purpose? The Principles of Chaos Engineering
In a monolithic application, failure is often an all-or-nothing event. In a distributed system, with dozens of services communicating over an unreliable network, partial failure is the normal state of operation. A single service can fail due to a bug, a hardware issue, or a network partition. Understanding how these small failures impact the broader system is one of the most critical aspects of microservice architecture.
To get into the right mindset, let's hear from the pioneers at Netflix, who popularized this discipline.
Mastering Chaos - A Netflix Guide to Microservices
This video, "Mastering Chaos - A Netflix Guide to Microservices," explains the core motivation behind resilience engineering.
First, watch the segment on the disaster scenario. This explains how the failure of a single, unprotected dependency can cause a cascading failure that takes down your entire system. Next, watch the introduction to Chaos Monkey. This is Netflix's famous tool that randomly terminates production instances to ensure that the loss of any single node is a "non-event." Our experiment today will be a manual, targeted version of this.
The practice of intentionally testing for failure is called Chaos Engineering. It's not about causing random destruction; it's a scientific method for uncovering weaknesses.
Today's experiment will follow these steps:
- Define Steady State: Establish a baseline of normal, successful system behavior.
- Formulate a Hypothesis: Make an educated guess about what will happen when we introduce a specific failure.
- Inject Failure: Introduce a controlled failure into the system.
- Observe and Verify: Measure the impact of the failure and compare it to your hypothesis.
- Learn and Improve: Use the findings to make the system more resilient.
2. Experiment: Simulating a Payment Service Outage
For our experiment, we will simulate an outage of the Payment Service. This is an interesting choice because it's part of the asynchronous workflow, not the initial, user-facing request.
Step 1: Define Steady State & Formulate Hypothesis
-
Steady State: Our steady state is the successful end-to-end flow we verified in the last lesson. A client sends a request, gets a quick
201 Createdresponse, and behind the scenes, the order event is processed by both the Inventory and Payment services, resulting in a new record in thepaymentsdatabase. -
Hypothesis: What do we predict will happen when the
payment-serviceis down?- Synchronous Flow: The initial
POST /ordersrequest to the API Gateway should still succeed. The Order Service doesn't talk to the Payment Service directly; it only publishes an event to Redis. The client should be unaware of any problem. - Asynchronous Flow: The
OrderCreatedevent will be published to theorder_eventsstream in Redis. The Inventory Service should consume it and work normally. The Payment Service, being offline, will not consume the event. - System State: After the experiment, we expect to see the event in Redis, but no corresponding entry in the
paymentsdatabase table. - Recovery: When we bring the
payment-serviceback online, our consumer group logic should allow it to read the "pending" message from the stream and process the payment, achieving eventual consistency.
- Synchronous Flow: The initial
Let's test this hypothesis.
Step 2: Prepare and Run the Experiment
First, ensure your environment is clean and running.
-
Start the System: In the root of your project, run:
docker-compose up --build -d -
Monitor the Logs: Just like last time, open separate terminal windows to tail the logs of our key services. This is our observation deck.
# Terminal 1 docker-compose logs -f api-gateway # Terminal 2 docker-compose logs -f order-service # Terminal 3 docker-compose logs -f inventory-service # Terminal 4 (This one will be quiet for a while) docker-compose logs -f payment-service -
Inject the Failure: Now, let's take down the Payment Service. In a new terminal, run:
docker-compose stop payment-serviceVerify that it has stopped by running
docker-compose ps. You should see its state asExited. -
Trigger the Flow: With the Payment Service down, run the
test_client.pyscript from the previous lesson to create an order.python test_client.py
Step 3: Observe and Verify the Impact
Carefully examine your logs and the state of the system.
-
Client & Synchronous Logs: Your
test_client.pyscript should report✅ Success!. The logs forapi-gatewayandorder-serviceshould show the normal, successful synchronous flow. This confirms the first part of our hypothesis: the user-facing interaction is unaffected. -
Asynchronous Logs:
- The
inventory-servicelog should show that it consumed theOrderCreatedevent. - The
payment-servicelog should remain silent. It's offline and cannot process the event.
- The
-
System State Verification:
- Check Redis: Confirm the message is waiting in the stream. The
order_eventsstream should have one message, and the consumer group should have one pending message for thepayment-consumer.
This command will show you that there is a message that has been delivered but not yet acknowledged.# Check for pending messages in the group docker-compose exec redis redis-cli XPENDING order_events payment-group - Check the Database: Connect to the
paymentsdatabase and verify that no new payment record was created.
You should not see a record for the order you just placed.docker-compose exec postgres psql -U your_db_user -d payments_db -c "SELECT * FROM payments ORDER BY created_at DESC LIMIT 5;"
- Check Redis: Confirm the message is waiting in the stream. The
Our observations match our hypothesis perfectly. The system has gracefully degraded.
Step 4: Simulate Recovery
Now, let's see if the system can heal itself. Bring the Payment Service back online.
docker-compose start payment-service
Immediately switch to the payment-service log terminal. You should see it spring to life! It will connect to Redis, find the pending message in its consumer group, and process the payment.
Finally, verify the database one last time:
docker-compose exec postgres psql -U your_db_user -d payments_db -c "SELECT * FROM payments ORDER BY created_at DESC LIMIT 5;"
The payment record should now be present. We have achieved eventual consistency.
3. Analysis: What Did We Learn?
This simple experiment reveals profound characteristics of our architecture:
- Decoupling works: By using a message broker (Redis Streams), the
order-service(the producer) is completely decoupled from thepayment-service(the consumer). The producer doesn't know or care if the consumer is online. This is the foundation of resilience in event-driven systems. - Graceful Degradation: The system didn't crash. The most critical part of the user experience—placing the order—remained fully functional. The backend processing was delayed, not lost.
- Identifying Criticality: We've implicitly identified the
payment-serviceas a non-critical component for the initial synchronous request. What would have happened if we stopped theorder-serviceinstead? The entire flow would have failed immediately. This makes theorder-servicepart of the critical path.
To see a practical implementation of injecting a different kind of failure—latency—and observing its effects, this article on ToxiProxy is an excellent read.
Chaos Engineering with ToxiProxy
This article provides a hands-on example of using a tool called ToxiProxy to inject network latency and test application timeouts.
Start by reading the introduction to understand the goal. Skim to the section under the heading "As you can see, our application was not configured to handle timeouts...". This demonstrates the 'loading screen of death' that occurs when a service doesn't have proper timeouts. This is a common consequence of a slow or unresponsive dependency. Finally, read how the author adds a timeout to the client application to fix the issue, making the system more resilient. This shows the "Learn and Improve" step of the chaos engineering cycle in action.
Conclusion: A Foundation for Resilient Systems
Congratulations on completing not just this lesson, but the entire course! Today, you moved beyond testing for success and deliberately tested for failure. You demonstrated that our event-driven architecture provides tangible resilience, allowing parts of the system to fail without causing a total outage. This is a fundamental skill for any engineer building distributed systems.
Key Takeaways from this Lesson:
- Failure in distributed systems is inevitable; we must design and test for it.
- Chaos Engineering is the practice of injecting controlled failures to verify system resilience.
- Asynchronous, event-driven communication is a powerful pattern for decoupling services and enabling graceful degradation.
- Understanding the "critical path" of a request helps prioritize which services must have the highest availability.
This project has given you a solid, practical foundation. You've journeyed from the high-level concepts of microservices to the low-level details of writing async gRPC code, containerizing services, orchestrating them with Docker Compose, and testing them for both success and failure. You now possess the core vocabulary, patterns, and hands-on skills to confidently design, build, and reason about modern, high-performance distributed systems in Python.