Let us build foundational phase with one of the most notoriously difficult problems in distributed architecture: Distributed Transactions.
When you break a monolith into microservices, you also shatter the database. You lose the magical SQL COMMIT and ROLLBACK that guarantees "all or nothing." If the Billing Service succeeds but the Inventory Service crashes, you have a massive data inconsistency.
Now, we learn how to keep distributed systems perfectly synchronized using Two-Phase Commit (2PC) and the Saga Pattern.
Scenario: You have a legacy system with two separate relational databases. Database A handles Orders. Database B handles Payments. The Problem: An order must be saved, and the payment must be deducted. If either fails, neither can be saved. Task: Guarantee ACID (Atomicity, Consistency, Isolation, Durability) properties across two physically separate network nodes.
We introduce a central "Coordinator" node to manage a strict, synchronous transaction.
The Workflow:
- Phase 1 (Prepare): The Coordinator asks both DB A and DB B: "Are you ready to commit?" 2. The Lock: DB A and DB B lock their respective rows. They write the intent to their local logs and reply: "Yes, prepared."
- Phase 2 (Commit): Once the Coordinator gets a "Yes" from both, it sends the final command: "Commit!" Both databases finalize the write and release their locks.
The Blocking Vulnerability (Why we don't use 2PC at scale): 2PC is conceptually easy but architecturally dangerous. It is a blocking protocol.
- If DB A says "Yes", it locks the row.
- If the Coordinator crashes immediately after receiving that "Yes" (before sending the final "Commit"), DB A is stuck. It cannot release the lock. It cannot accept new writes to that row.
- The Verdict: 2PC is fine for internal, low-throughput systems, but it is too brittle and slow for massive, high-concurrency cloud architectures. We need something asynchronous.
Scenario: You are building the backend for a collaborative, lightweight file system application where colleagues can share and build rules around documents. The Problem: When a user uploads a file, a complex chain must happen:
Storage Servicesaves the raw file to S3.Security Servicescans it for malware.Metadata Serviceupdates the collaborative folder view. The Disaster: The file is saved, the malware scan passes, but theMetadata Servicecrashes. The file exists in storage, but nobody can see it in the UI. We cannot use 2PC here because S3 doesn't support it. Task: Design an eventually consistent transaction that can roll itself back if a step fails.
We use an event-driven architecture powered by Apache Kafka. There is no central coordinator. Each service reacts to events and emits new ones.
The Workflow (Happy Path):
Storage Servicesaves the file and publishes an event to Kafka:FileUploaded.Security ServiceconsumesFileUploaded, scans it, and publishesScanPassed.Metadata ServiceconsumesScanPassedand updates the UI database.
The Workflow (Failure & Compensation):
Storage ServicepublishesFileUploaded.Security Serviceconsumes it, finds a virus, and publishesScanFailed.- The Compensation: The
Storage Serviceis configured to listen forScanFailed. When it sees this event, it executes a compensating transaction: it permanently deletes the raw file from S3.
The "Phantom Read" Problem: In a Saga, the transaction isn't isolated. For a few milliseconds, the file is in S3 before the malware scan finishes.
- The Fix: We use state flags. The
Storage Servicesaves the file withstatus = PENDING. The UI is programmed to completely ignore files in thePENDINGstate. Only when the finalMetadataUpdatedevent loops back does the status change toAVAILABLE.
Scenario: You are executing a massive data pipeline involving multiple clusters.
- Step 1: Provision temporary AWS EC2 nodes.
- Step 2: Run an Apache Flink streaming job.
- Step 3: Write results to ClickHouse. The Problem: If Choreography (Medium scenario) gets too complex, it turns into "Event Spagetti." If Step 3 fails, figuring out which service needs to trigger which compensation event to tear down the EC2 nodes becomes a debugging nightmare because the logic is scattered across 10 different codebases. Task: Design a transaction that is asynchronous but centrally managed and strictly tracked.
We introduce an Orchestrator (often implemented via a state machine or a DAG runner). The Orchestrator tells the microservices what to do, rather than the services reacting to each other.
The Architecture:
- We use a central commander (while tools like Apache Airflow are excellent for scheduling batch DAGs, for transactional micro-second orchestration we use systems like AWS Step Functions, Temporal.io, or Netflix Conductor).
The Workflow:
- Command: The Orchestrator sends a command via a message queue:
ProvisionNodes. - Reply: The Node Service provisions them and replies to the Orchestrator:
NodesReady. - Command: Orchestrator sends
StartFlinkJob. - Failure Reply: Flink Service replies:
JobFailed (Out of Memory). - The Rollback: The Orchestrator looks at its internal state machine, sees that it needs to undo Step 1, and explicitly sends a
TerminateNodescommand to the Node Service.
Production Grade Architecture
The Idempotency Requirement:
What if the Orchestrator sends the TerminateNodes command, but the network drops the acknowledgment? The Orchestrator will retry and send TerminateNodes again.
- Every single microservice participating in an Orchestrated Saga must be perfectly idempotent (which we solved back on Day 14). If the Node Service receives
TerminateNodestwice, it must safely ignore the second request rather than throwing an error that could halt the rollback process.