What Are Microservices, Event-Driven Architecture, and Multi-Zone Deployment?
Learn what microservices, event-driven architecture, and multi-zone deployment are, how they work together, and which trade-offs to weigh before you adopt them.

Your application is growing, and so are the risks. One slow module forces you to scale everything, one bad deploy blocks every team, and one failed data center can take the whole product offline. Microservices and event-driven architecture, combined with multi-zone deployment, are three common answers to those problems.
They solve different problems, and teams often mix them up. This post explains what each one is, shows how they fit together in one example system, and covers the trade-offs you accept when you adopt them. By the end, you can decide which of the three your system needs and which it does not.
Quick Answer
Microservices split an application into small, independently deployable services that each own one business capability. Event-driven architecture lets those services communicate by publishing and consuming events, so they react to what happened instead of calling each other directly. Multi-zone deployment runs the services and their data across several availability zones in one cloud region, so the loss of a single data center does not stop the system. Together they give you independent scaling, loose coupling, and resilience to zone failures, in exchange for more operational complexity and eventual consistency.
What Are Microservices, Event-Driven Architecture, and Availability Zones?
Microservices
A microservice is a small service that owns one business capability, such as orders, payments, or inventory. You deploy it on its own, scale it on its own, and give it its own data store. Services talk to each other over the network, either with direct calls such as HTTP or gRPC, or with messages.
Martin Fowler's article on microservices is the reference most teams start from.
Event-driven architecture
In an event-driven architecture, a service publishes an event when something happens, for example OrderPlaced. Other services subscribe to that event and react in their own time. The publisher does not know who is listening, and that is the main source of the loose coupling.
Events usually travel through a broker such as Apache Kafka, RabbitMQ, or a cloud queue service. The term covers several different patterns, and Fowler's breakdown of what "event-driven" means helps you pick the right one.
Multi-zone deployment
A cloud region is divided into availability zones. Each zone is a physically separate data center, or group of data centers, with its own power, cooling, and networking, and the zones in a region are linked by fast connections. A multi-zone deployment runs your workloads in at least two zones, and preferably three, so losing one zone does not stop the application. AWS documents its regions and availability zones, and Google Cloud and Azure use the same idea.
Multi-zone is not multi-region. All the zones sit in one geographic region, so you survive a data center failure but not a regional outage.
| Concept | Question it answers | Typical tools |
| :---- | :---- | :---- |
| Microservices | How do you split and deploy the application? | Containers, Kubernetes, API gateways |
| Event-driven architecture | How do the parts communicate without tight coupling? | Kafka, RabbitMQ, SNS and SQS |
| Multi-zone deployment | How do you survive a data center failure? | Load balancers, replicated databases, zone-aware scheduling |
The Problem
A monolith is the right starting point for most products. Over time, though, it couples everything together. You scale the whole application to relieve one hot module, and every release needs coordination between teams.
Splitting the monolith into microservices fixes the deployment problem and often creates a new one. Imagine checkout calls inventory, inventory calls payment, and payment calls notifications, all synchronously. If notifications slows down, payment waits, then inventory waits, then your customer stares at a spinner. You now have a distributed monolith: the same coupling as before, with network failures added.
There is a third coupling that has nothing to do with code. If all your services run in one availability zone, a power or network failure in that zone takes every one of them down together, however well you designed them. You have three kinds of coupling to remove: coupling in code, coupling at runtime, and coupling in infrastructure.
The Solution
Each technique removes one of those couplings. Microservices break the code apart, events break the runtime dependency, and multiple zones break the shared infrastructure. The example below is an online store, where an order triggers inventory, payment, and notification work.

Step 1: Split by business capability
Draw service boundaries around things that change together, such as orders, payments, and inventory. Give each service its own database or schema, and never let two services read each other's tables. Shared tables bring back the coupling you are trying to remove.
Start with the fewest services that give your teams independence. You can always split a service later, but merging two badly drawn services is painful.
Step 2: Publish events instead of chaining calls
The order service records the order and publishes OrderPlaced. It does not wait for inventory, payment, or notifications. Each of those services consumes the event on its own schedule, so a slow consumer no longer slows the customer down.

The script below runs in Node.js and uses an in-process event emitter as a stand-in for a broker. Save it as order-events.ts and run npx tsx order-events.ts.
import { EventEmitter } from "node:events";
import { randomUUID } from "node:crypto";
type OrderPlaced = {
eventId: string;
type: "OrderPlaced";
orderId: string;
items: { sku: string; qty: number }[];
};
const bus = new EventEmitter();
// Each consumer remembers the events it has already handled.
function subscribe(consumer: string, handler: (event: OrderPlaced) => void) {
const seen = new Set<string>();
bus.on("OrderPlaced", (event: OrderPlaced) => {
if (seen.has(event.eventId)) {
console.log(`${consumer}: duplicate ${event.eventId} ignored`);
return;
}
seen.add(event.eventId);
handler(event);
});
}
subscribe("inventory", (e) =>
e.items.forEach((i) => console.log(`inventory: reserve ${i.qty} x ${i.sku}`))
);
subscribe("notifications", (e) =>
console.log(`notifications: confirmation sent for order ${e.orderId}`)
);
// Order service
function placeOrder(orderId: string, items: OrderPlaced["items"]): OrderPlaced {
const event: OrderPlaced = { eventId: randomUUID(), type: "OrderPlaced", orderId, items };
bus.emit("OrderPlaced", event);
return event;
}
const event = placeOrder("order-1001", [{ sku: "keyboard", qty: 2 }]);
bus.emit("OrderPlaced", event); // simulates a broker redelivering the same eventThe second emit simulates a redelivery. Both consumers print a "duplicate ignored" line instead of reserving stock or sending a second email.
Step 3: Make events safe to process
Most brokers deliver events at least once, so your consumers will see duplicates. Give every event a unique ID and make each consumer idempotent, as the script above does. In production, store the processed IDs in your database, not in memory.
Publishing has its own trap. If the order service saves to its database and then publishes to the broker, a crash between the two steps loses the event. The transactional outbox pattern solves this by writing the event to an outbox table in the same database transaction, and a separate relay publishes it to the broker.
Finally, remember that Kafka guarantees ordering only within a partition. Use the order ID as the message key so all events for one order land in the same partition and stay in sequence.
Step 4: Spread services and data across zones
In Kubernetes, a topology spread constraint tells the scheduler to balance pods across zones. The manifest below spreads six replicas of the order service evenly. Replace the image with your own.
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
replicas: 6
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: order-service
containers:
- name: order-service
image: registry.example.com/order-service:1.0.0
ports:
- containerPort: 8080
Apply it, then check where the pods landed. Match each node name in the first output against the zone label in the second.
kubectl apply -f order-service.yaml
kubectl get pods -l app=order-service -o wide
kubectl get nodes -L topology.kubernetes.io/zoneThe Kubernetes documentation explains topology spread constraints in full.
Your broker needs the same treatment. Set broker.rack on each Kafka broker to its zone name, create topics with three replicas, and require two of them to acknowledge each write.
kafka-topics.sh --bootstrap-server localhost:9092 --create \
--topic orders.placed \
--partitions 6 \
--replication-factor 3 \
--config min.insync.replicas=2Pair this with producers configured with acks=all. The Kafka documentation covers rack awareness and replication in detail. For your relational database, use your provider's multi-zone option, which keeps a standby in another zone and fails over automatically.
Two sizing rules matter here. First, quorum-based systems such as Kafka's KRaft controllers and etcd need a majority of nodes alive, so use three zones. With two zones, losing the zone that holds the majority stops writes. Second, if you run across three zones and want to survive losing one, keep each zone below about two thirds of its capacity so the other two can absorb the traffic.

Step 5: Test a zone failure
A multi-zone setup you have never tested is a hope, not a design. In a staging cluster, drain every node in one zone and watch what happens to traffic, consumer lag, and error rates. Replace the zone name with one from your cluster.
for node in $(kubectl get nodes -l topology.kubernetes.io/zone=us-east-1a -o jsonpath='{.items[*].metadata.name}'); do
kubectl drain "$node" --ignore-daemonsets --delete-emptydir-data
doneA drain is a graceful approximation of a failure, not a real outage. It still shows whether your replicas, broker, and database recover without manual work.
Trade-offs
Operational complexity
One deployable becomes many services, pipelines, and dashboards. You need distributed tracing, centralised logging, and service-level alerts before you need them badly. A small team can spend more time running the platform than building the product.
Eventual consistency and harder debugging
With events, no single moment exists when the whole system is consistent. The order exists before the stock is reserved, and your UI must handle that gap. Debugging also changes, because a request now travels through a broker and several consumers instead of one stack trace.
Cost and latency of crossing zones
Traffic between zones adds latency, and many cloud providers bill for data that crosses zone boundaries. Spare capacity for failover also costs money every day, even when no zone fails. Zone-aware routing and consumer placement reduce the bill but add configuration to maintain.
What multi-zone does not cover
Multi-zone protects you from a data center failure. It does not protect you from a regional outage, a bad deployment, a corrupted database, or a bug that publishes the wrong events. For regional failures you need a multi-region design, which is a separate and much larger project.
When this is the wrong approach
If you have a small team, one product, and modest availability needs, a well-structured monolith across two zones is often the better choice. You can add events at the one boundary that hurts, such as email or billing, without adopting everything at once. Reach for full microservices when independent deployment and scaling are real needs and not a fashion.
Conclusion
Microservices remove coupling in code, events remove coupling at runtime, and multi-zone deployment removes coupling in infrastructure. You do not need all three on day one. On Monday, pick one capability that hurts most, publish its events through a broker with idempotent consumers, and check how many zones your current workloads actually run in. Then drain one zone in staging and see what breaks.
Need help designing or running a distributed architecture like this? Contact us

Aarav Sharma
Lead Software Engineer
Aarav leads product engineering at Matlab Infotech, where he has shipped mobile and web platforms across healthcare, fintech, and SaaS. He writes about pragmatic engineering and shipping fast without cutting corners.


