- A monolith is one deployable program; a modular monolith is one deployable with strict internal boundaries; microservices are many small, independently deployable programs that each own one business capability and its data.
- Instagram famously started as — and, per its public engineering talks, still largely runs as — a very large Python/Django monolith. Read the diagram's "services" as logical domains; some are separate deployables, many are modules inside one big codebase.
- Application servers must be stateless: any instance can serve any request because all state lives in databases, caches and session stores. That is what makes horizontal scaling, autoscaling and painless deploys possible.
- Once you split, you inherit distributed-systems problems: service discovery, sync vs async communication, data ownership, and no cross-service ACID transactions — solved with sagas and the transactional outbox, not 2PC.
- Operating many services needs containers, Kubernetes, autoscaling, canary/blue-green deploys, feature flags and often a service mesh. Split only when team size and scaling needs justify the cost (Conway's law).
1. The problem: why split a system into services at all?
Picture Instagram in October 2010: a handful of engineers, one Django application, one PostgreSQL database. Every feature — sign-up, photo upload, the feed, likes, comments, follows — lived in one codebase and ran in one process type. That is a monolith, and it was exactly the right choice: one thing to build, test, deploy and debug.
Now fast-forward to a company with thousands of engineers and 2+ billion monthly users. New problems appear that have nothing to do with any single feature:
- Team coupling. Hundreds of engineers commit to one codebase daily; a bug in comment ranking can block the Stories team's deploy.
- Uneven scaling. The feed is read tens of thousands of times per second; "change password" a few times a second. A monolith scales everything together.
- Blast radius. A memory leak in the upload path can crash the same process that serves likes.
- Technology fit. A messaging gateway holding millions of WebSocket connections wants a different runtime than a CRUD API in Python.
Microservices are one answer: split the system along business boundaries into separately deployable services, each owned by one team. But — the most important sentence in this lesson — splitting trades code complexity for operational and distributed-systems complexity. Function calls become network calls that can time out; one database transaction becomes several databases that can disagree.
2. Monolith vs modular monolith vs microservices
2.1 Definitions
- Monolith — a single deployable unit (one Django project, one image) containing all features, usually over one shared database. The defining trait: built, deployed and scaled as one.
- Modular monolith — still one deployable, but divided into modules with explicit interfaces (
feed.api,graph.api), enforced import rules and per-module table ownership. Calls stay in-process, yet a module could later be extracted. - Microservices — many independently deployable services, each owning one bounded context (a business capability with its own language and data, e.g. "social graph") and its own datastore, communicating only via APIs (REST, gRPC) or events (Kafka).
2.2 Side-by-side comparison
| Dimension | Monolith | Modular monolith | Microservices |
|---|---|---|---|
| Deployable units | 1 | 1 | Tens to thousands |
| Calls between features | In-process function calls (ns–µs) | In-process calls through module interfaces | Network calls (≈0.5–5 ms each), can fail or time out |
| Data | One shared schema; joins everywhere | One DB, but tables owned per module; cross-module joins discouraged | Database per service; no cross-service joins |
| Transactions | Easy: one ACID transaction | Easy (same DB), but discouraged across modules | No cross-service ACID; use sagas, outbox, idempotency |
| Scaling | Scale everything together | Scale everything together (can route endpoint pools separately) | Scale each service independently |
| Team autonomy | Low — shared release train | Medium — clear ownership, shared release | High — independent releases |
| Failure isolation | Poor — one bad path can crash the process | Poor to medium | Good, if timeouts and circuit breakers are used |
| Operational cost | Lowest | Low | High: discovery, tracing, mesh, many pipelines |
| Best for | Start-ups, small teams, unclear domains | Most companies, most of the time | Large orgs with many teams and very different scaling needs |
2.3 Why a monolith scaled so far for Instagram
The key insight: "monolith" describes how code is packaged, not how it scales. A stateless monolith can be copied onto 10,000 servers behind a load balancer just as easily as a microservice. What it can't easily do is let 200 teams deploy independently — Instagram compensated with tooling (continuous deployment with canaries, code ownership, static analysis enforcing boundaries, feature flags). Meanwhile the data tier was split aggressively: sharded PostgreSQL, Cassandra, Memcached/Redis. Scale comes mostly from the data layer and async workers.
3. Benefits and costs of microservices
3.1 Benefits
🚀 Independent deployment
Stories ships a fix at 3 pm without waiting for Feed's release. Smaller deploys, faster rollback.
📈 Independent scaling
3,000 instances of the feed read path, 50 of account settings; instance types per workload.
🧯 Fault isolation
Search down? The feed still loads (only with timeouts, circuit breakers and fallbacks).
🧰 Technology freedom
Each service picks its runtime and datastore.
👥 Ownership
"You build it, you run it": code, data, dashboards and on-call per team.
3.2 Costs (the part people underestimate)
- Network unreliability. The fallacies of distributed computing — "the network is reliable", "latency is zero", "bandwidth is infinite", "topology doesn't change" — are all false. Every call needs a timeout, retry policy and fallback (see Resilience patterns).
- Availability multiplies down. If a request synchronously needs 10 services that are each 99.9% available, the request succeeds only
0.99910 ≈ 99.0%of the time — about 7 hours of failure per month instead of 43 minutes. - Tail latency amplifies. If each of 10 parallel downstream calls is slow 1% of the time, the chance that at least one is slow is
1 − 0.9910 ≈ 9.6%. Your p99 becomes your p90. - No more cross-feature transactions or joins. Consistency across services is eventual; you need sagas, outboxes and idempotent consumers (sections 8–9).
- Operational overhead and harder debugging. Every service needs CI/CD, dashboards, on-call and a versioned API; one request may touch 30 services, so you need distributed tracing.
4. Stateless services
4.1 What "stateless" means
A service is stateless when an instance keeps no data between requests that another instance would need. Every request carries (or can look up) everything required to process it: an auth token identifying the user, the IDs it operates on, and access to shared datastores. The instance's memory and local disk are scratch space only — losing the instance loses nothing important.
By contrast, a stateful server keeps something meaningful locally: a user's login session in memory, an uploaded file on local disk, a counter it increments, an open WebSocket connection, a partition of a database.
4.2 Why statelessness matters so much
- Horizontal scaling is trivial. Need more capacity? Start 200 more identical instances and register them with the load balancer. No data needs to move.
- Any instance can serve any request. The load balancer can use simple round-robin or least-connections — no sticky sessions needed. Load spreads evenly.
- Failure is cheap. If an instance dies, the load balancer's health check removes it and in-flight requests are retried elsewhere. Nobody loses their login.
- Deploys and autoscaling are painless. Rolling deploys replace instances one batch at a time; autoscalers add and remove instances at will (section 10).
4.3 Where the state goes instead
State doesn't disappear — it is pushed down into systems designed to hold it durably and share it:
| Kind of state | Naive (stateful) place | Where it should live | Instagram example |
|---|---|---|---|
| Login session | In-memory session dict on one server | Signed token (JWT) carried by the client, or a session ID looked up in a shared store (Redis/DB) | Auth token validated at the API gateway / Auth service |
| Business data | Local SQLite or files | Databases (source of truth) | Users in sharded PostgreSQL; posts in Cassandra |
| Hot derived data | Per-process dictionaries | Shared cache (Redis/Memcached); small per-process caches are OK as an optimisation only | Feed timelines and counters in Redis |
| Uploaded files | Local disk of the server that received them | Object storage | Photos uploaded straight to S3 via pre-signed URLs |
| Background work | Threads inside the web process | Durable queue/log + workers | Kafka → fan-out, media processing |
| Long-lived connections | — | A dedicated, deliberately stateful connection tier with a registry mapping user → gateway | DM WebSocket gateways |
4.4 Sticky sessions — the tempting shortcut
A load balancer can pin a user to one instance ("session affinity"). It lets stateful code limp along, but load becomes uneven and scale-in or deploys log people out. Use stickiness only for performance, never correctness. See Load balancing.
5. Service discovery: how does service A find service B?
Instances change IPs every minute under autoscaling. Service discovery maps a logical name ("graph-service") to the current set of healthy addresses.
5.1 The service registry
A service registry is a highly available database of "service name → instances (IP, port, health)". Instances get into it by:
- Self-registration: the instance registers on start-up and sends heartbeats; if they stop, the entry expires after a TTL (ZooKeeper's ephemeral znodes vanish when the session dies).
- Third-party registration: the platform does it — Kubernetes tracks which pods pass their readiness probes and updates each Service's endpoints.
Because everything depends on it, the registry runs as a 3- or 5-node consensus cluster: ZooKeeper (ZAB; ephemeral nodes + watches, also leader election), etcd (Raft; the brain of Kubernetes) or Consul (Raft; built-in health checks, DNS interface, multi-DC). Netflix Eureka deliberately chooses availability, serving possibly-stale lists during partitions. Clients cache the last known list so a registry outage doesn't instantly break traffic.
5.2 Client-side vs server-side discovery
CLIENT-SIDE DISCOVERY SERVER-SIDE DISCOVERY
Feed svc Registry Feed svc
| (1) where is graph-svc? | | (1) GET http://graph-svc/...
|-------------------------->| v
| (2) [10.0.1.5, 10.0.1.9, | +-----------+ watches +----------+
| 10.0.2.3] | | LB / VIP |<------------| Registry |
|<--------------------------| | (or K8s | +----------+
| (3) client picks one | | Service) |
| (round robin / P2C) | +-----------+
|---------> 10.0.1.9 | (2) forwards to a healthy instance
v
10.0.1.9
| Client-side discovery | Server-side discovery | |
|---|---|---|
| Who chooses the instance | The calling service, using a smart client library | A load balancer / proxy in the middle |
| Extra network hop | None — direct instance-to-instance | Yes (through the LB), unless the proxy runs as a local sidecar |
| Load-balancing smarts | Rich: least-outstanding-requests, power-of-two-choices, zone awareness | Whatever the LB supports |
| Language burden | Every language needs the client library (Java, Python, Go…) | Clients stay dumb; just call a hostname |
| Examples | Netflix Eureka + Ribbon, gRPC client-side LB, Finagle | AWS ALB/NLB, Kubernetes Service (kube-proxy), Envoy/Istio sidecars |
A service mesh (section 11) is effectively a hybrid: the "client" is a local sidecar proxy, so the application calls localhost, while the sidecar does smart client-side balancing using endpoint lists pushed from a control plane.
5.3 DNS-based discovery
The simplest registry is DNS: graph-service.prod.internal resolves to healthy instances (A records; SRV records add ports and weights). Consul exposes a DNS interface; Kubernetes' CoreDNS resolves graph.default.svc.cluster.local to a Service VIP. It works in every language for free, but resolvers cache answers for the TTL (some ignore it), so removed instances keep getting traffic, and DNS carries no load information. Hence DNS usually finds a load balancer or VIP that does health-aware balancing.
6. How services talk: synchronous vs asynchronous
6.1 Synchronous request/response (REST, gRPC)
The caller sends a request and waits — needed when it can't continue without the result (the Feed service needs post metadata and counts to build its response).
- REST over HTTP/JSON — universal and easy to debug; verbose, informal schemas.
- gRPC — HTTP/2 + Protocol Buffers: compact binary, generated stubs, strict evolvable schemas, streaming, deadline propagation. The default for internal calls at scale (Meta's equivalent is Thrift).
See API design. Every sync call needs a timeout (ideally a shrinking deadline budget), bounded retries with backoff and jitter, and a circuit breaker — otherwise one slow service exhausts upstream thread pools (cascading failure).
6.2 Asynchronous messaging (events)
The producer publishes to a broker (Kafka, RabbitMQ, SQS) and does not wait. Commands/jobs ("resize this photo") go to a queue for one worker; domain events (PostCreated{post_id: 42, author_id: 7}) are facts about the past that any number of unknown consumers can react to.
That's the Instagram diagram: PostCreated goes to Kafka and fan-out workers, the search indexer, notifications and analytics consume it on their own schedule. If the indexer is down for 10 minutes, posts still publish; it catches up from the log. See Message queues & Kafka.
6.3 Choosing between them
| Synchronous (REST/gRPC) | Asynchronous (events/queues) | |
|---|---|---|
| Coupling | Temporal: both must be up at once | Decoupled in time; broker buffers |
| User latency | Adds up along the chain | Fast "accepted"; work happens later |
| Consistency | Result visible immediately | Eventual (seconds) |
| Failures | Timeouts, retries, circuit breakers | Redelivery, dead-letter queues, idempotent consumers |
| Load spikes | Hit every downstream immediately | Queue absorbs them (load levelling) |
| Feed read → post metadata, counts, "has viewer liked" | PostCreated → fan-out, search, notifications; LikeCreated → notifications |
7. Data ownership: database per service
7.1 The rule
Each service owns its data exclusively. Only the Graph service touches follow tables; only the Like service touches likes. Others go through the owner's API or consume its events. Each picks its own store: a TAO-like association store for Graph, Redis for Feed, PostgreSQL for Users, Elasticsearch for Search.
Why so strict? A shared database is a hidden, unversioned API: if Feed runs SELECT … FROM likes JOIN posts … directly, the Like team can never change schema, move to Cassandra or reshard without breaking Feed — and shared tables mean shared load.
7.2 But then how do I query across services?
- API composition — the caller (or an API gateway / BFF) calls services in parallel and stitches results: feed rendering gets post IDs from the timeline, then batch-fetches metadata, profiles, counts and "viewer has liked" flags.
- Replicated read models (CQRS) — subscribe to others' events and keep a denormalised copy: Search builds Elasticsearch from
UserUpdated/PostCreatedand never queries the User DB. - Denormalise into the event —
PostCreatedcarries author_id, timestamp, media type and audience so consumers needn't call back.
8. Distributed transactions: 2PC vs sagas
In one database, "do A and B atomically" is BEGIN; …; COMMIT;. Across services with separate databases there is no shared transaction.
8.1 Two-phase commit (2PC)
A coordinator asks every participant to prepare (write durably, hold locks, promise to commit). If all vote "yes" it tells everyone to commit; otherwise everyone aborts.
Coordinator Post DB User DB
|--- PREPARE -------->| |
|--- PREPARE -------------------------------> |
|<-- YES (locks held) | |
|<-- YES (locks held) ------------------------ |
| write "COMMIT" to coordinator log |
|--- COMMIT --------->| |
|--- COMMIT ---------------------------------->|
|<-- ACK ------------ |<-- ACK ----------------|
If the coordinator crashes after PREPARE, participants are stuck holding
locks ("in doubt") until it recovers — 2PC is a BLOCKING protocol.
Problems at scale: blocking (locks held across round trips; stuck if the coordinator dies), availability (every participant must be up), latency (two round trips plus forced log writes), and support (many NoSQL stores and brokers don't speak XA/2PC). It's used inside engineered databases (Spanner and CockroachDB run 2PC over Paxos/Raft groups so the coordinator can't vanish), rarely between microservices.
8.2 Sagas
A saga breaks a business transaction into a sequence of local transactions, each committed in one service's own database. If a later step fails, the saga runs compensating transactions that semantically undo the earlier steps (you can't roll back a committed transaction, but you can apply its opposite). Sagas give up isolation — other requests can see intermediate states — in exchange for availability and loose coupling. This is why posts have explicit states like PROCESSING.
8.3 Instagram example: publishing a post as a saga
Publishing touches several domains. Suppose these steps (simplified):
- Post service: insert post row with
status = PROCESSING(local transaction) → emitPostSubmitted. - Media processing: generate renditions in S3 → emit
MediaReady(orMediaFailed). - Integrity / moderation: classify the media and caption → emit
ContentApproved(orContentRejected). - Post service: set
status = PUBLISHED; User service increments the author'spost_count→ emitPostCreated. - Fan-out, search, notifications: consume
PostCreated.
Compensations: if moderation rejects the post after it was published (e.g. a delayed review), the saga sets status = REMOVED, decrements post_count, emits PostRemoved so fan-out workers delete the ID from timelines and the indexer drops it, deletes renditions, and notifies the author. If media processing fails, the post goes to FAILED and the client is told to retry. Notice that compensations must be idempotent and always succeed eventually (retry until done).
8.4 Choreography vs orchestration
| Choreography | Orchestration | |
|---|---|---|
| How | Services subscribe to each other's events and react | A coordinator (workflow engine) sends commands and tracks state |
| Coupling | Very loose; easy to add a new consumer | Services depend on the orchestrator's commands |
| Visibility | The flow is implicit — hard to answer "where is post 42 stuck?" | Explicit state per saga instance; easy to monitor and retry |
| Risk | Cyclic event chains, "event spaghetti" | Orchestrator becomes a bottleneck or a "god service" |
| Fits | Simple, few-step flows; broadcast side effects (fan-out, search, notifications) | Long, multi-step, business-critical flows with compensations |
| Tools | Kafka, SNS/SQS, any pub/sub | Temporal, Cadence, AWS Step Functions, Camunda, Netflix Conductor |
9. The transactional outbox pattern
9.1 The dual-write problem
The Post service must update its database and publish PostCreated — two different systems — so this naive code is broken:
db.execute("UPDATE posts SET status='PUBLISHED' WHERE id=%s", post_id)
db.commit()
kafka.publish("post-events", PostCreated(post_id)) # what if we crash right here?
- Crash after commit, before publish → the post is published but no follower ever sees it in their feed, and it's never indexed.
- Publish first, then the DB commit fails → consumers fan out a post that doesn't exist.
9.2 The fix: write the event in the same local transaction
Add an outbox table to the service's own database. In one local ACID transaction, update the business row and insert the event into the outbox. A separate relay process reads the outbox and publishes to Kafka, marking rows as sent.
BEGIN;
UPDATE posts SET status = 'PUBLISHED' WHERE post_id = 42;
INSERT INTO outbox (event_id, aggregate_id, type, payload, created_at)
VALUES ('evt-9f1c', 42, 'PostCreated',
'{"post_id":42,"author_id":7,"media":"photo","audience":"followers"}', now());
COMMIT; -- both happen, or neither does
-- relay loop (separate process)
loop:
rows = SELECT * FROM outbox WHERE sent_at IS NULL ORDER BY created_at LIMIT 500
for r in rows: kafka.publish(topic="post-events", key=r.aggregate_id, value=r.payload)
kafka.flush() # wait for broker acks
UPDATE outbox SET sent_at = now() WHERE event_id IN (rows.ids)
- Atomicity of "state changed" and "event recorded" comes from the local transaction.
- The relay can crash between publishing and marking sent → at-least-once delivery, so consumers must be idempotent (dedupe on
event_id, or naturally idempotent ops like a sorted-set add). - Keying Kafka messages by post ID keeps a post's events ordered on one partition.
- CDC relays (Debezium) tail the replication log (PostgreSQL WAL, MySQL binlog) instead of polling.
The consumer-side mirror is the inbox / idempotent consumer: record processed event_ids in the same transaction as the state change.
10. Deploying and running many services
10.1 Containers
A container (Docker/OCI image) packages a service with its exact runtime, libraries and config into one immutable artifact. The same image runs on a laptop, in CI and in production — no more "works on my machine". Containers share the host OS kernel (unlike virtual machines), so they start in seconds and you can pack dozens onto one host. Each build produces a versioned image (e.g. feed-service:2026.10.23-ab12cd); deploying means "run this image version".
10.2 Kubernetes in five concepts
- Pod — one or more containers sharing a network namespace (e.g. app + sidecar proxy).
- Deployment — "run 40 replicas of
feed-service:v123"; Kubernetes reconciles reality to this desired state, rescheduling pods from dead nodes. - Service — a stable name and virtual IP in front of changing pods (server-side discovery).
- Probes — liveness (restart if wedged) and readiness (no traffic until warmed up).
- Requests/limits — CPU and memory per pod, for bin-packing and noisy-neighbour protection.
10.3 Autoscaling
Because services are stateless, capacity can follow demand. The Kubernetes Horizontal Pod Autoscaler (HPA) computes:
desiredReplicas = ceil( currentReplicas × currentMetric / targetMetric )
Example: 40 feed pods at 90% average CPU, target 60%
→ ceil(40 × 90 / 60) = 60 pods
Metrics can be CPU, memory, requests per second or queue lag (e.g. scale fan-out workers on Kafka consumer lag). A cluster autoscaler adds or removes whole machines when pods can't be scheduled or nodes sit idle. Practical details matter: scale up fast, scale down slowly (avoid flapping); keep headroom because new instances need time to start and warm caches; and pre-scale for predictable peaks (evenings, New Year's Eve, big sports finals) rather than relying purely on reactive scaling.
10.4 Release strategies
| Strategy | How it works | Pros | Cons |
|---|---|---|---|
| Rolling update | Replace instances in batches (e.g. 10% at a time) with the new version | No extra capacity needed; default in Kubernetes | Both versions serve traffic during rollout; rollback is another rolling update |
| Blue/green | Run the full new version ("green") beside the old ("blue"); flip the load balancer to green at once; keep blue for instant rollback | Instant switch and instant rollback; test green with real infra before cut-over | Needs 2× capacity during release; DB schema must work for both versions |
| Canary | Send a small slice of traffic (1% → 5% → 25% → 100%) to the new version; compare error rates, latency and business metrics against the old version; halt automatically on regression | Limits blast radius; catches problems that only show under real traffic | Needs good metrics and automated analysis; slow for urgent fixes |
| Feature flags | Ship code "dark", then enable it at runtime for specific users, % of users, countries or employees | Decouples deploy from release; instant kill switch; enables A/B tests | Flag debt (stale flags), combinatorial testing, config itself can cause outages |
11. Service mesh and sidecars
With hundreds of services, every one of them needs the same plumbing: mutual TLS, retries, timeouts, circuit breaking, load balancing, metrics, tracing headers. Re-implementing these in every language's client library is painful. A service mesh moves this plumbing out of the application into a sidecar proxy (most commonly Envoy) that runs next to every instance — in the same Kubernetes pod — and intercepts all inbound and outbound traffic.
- Data plane — the sidecars carrying traffic: client-side load balancing, retry budgets, outlier ejection, uniform metrics for every hop.
- Control plane — distributes routes (5% to v2: a mesh-level canary), policy (only Feed may call Graph) and short-lived certificates for mTLS.
- Costs — an extra local proxy hop per side (sub-millisecond, but it adds up), CPU/memory per pod, and a complex system to operate; sidecar-less designs (Istio ambient, eBPF/Cilium) reduce this.
Meta built equivalent capabilities into shared RPC libraries (Thrift-based) long before off-the-shelf meshes existed; a mesh is the "buy" version of that "build".
12. Conway's law: architecture mirrors the org chart
"Any organization that designs a system will produce a design whose structure is a copy of the organization's communication structure." — Melvin Conway, 1968
If you have three teams building a compiler, you get a three-pass compiler. Applied to microservices: service boundaries follow team boundaries, whether you plan it or not. The practical lessons:
- Design teams and services together (the "inverse Conway manoeuvre"): want a Stories service? Create a Stories team that owns it end to end.
- One owner per service; a service owned by everyone is owned by no one.
- Small orgs shouldn't copy big-org architectures. Ten engineers running 40 services contradicts the org chart.
13. When to split — and when not to
13.1 Good reasons to extract a service
- Different scaling or runtime profile: feed reads at 100k+ QPS vs settings at 10 QPS; GPU transcoding; long-lived WebSocket connections.
- Different security requirements: auth and payments benefit from a small, tightly controlled surface.
- Team autonomy: a large team blocked by the shared release train.
- A clean bounded context with a narrow API and little shared data.
13.2 Bad reasons
- "Microservices are modern." Fashion is not a requirement.
- A small team, or domain boundaries still changing weekly — you'll cut in the wrong place.
- Messy code: a messy monolith split up becomes a messy distributed system. Modularise first.
- Entities constantly read and written together — you'll recreate joins over the network.
13.3 How to split safely: the strangler fig
- Modularise in place: define the module's API and stop other modules touching its tables.
- Route through a facade (gateway or in-code) and build the new service behind the same API; shadow traffic to compare responses.
- Migrate data: backfill, keep in sync via CDC, then move ownership.
- Shift traffic gradually (canary %), then delete the old code — the new service has "strangled" the old path like a strangler fig around a tree.
14. How Instagram uses microservices & stateless services
Putting it together, a realistic reading of the diagram:
- The web tier is stateless: thousands of identical Django servers behind load balancers; tokens for auth, data in sharded PostgreSQL/Cassandra/caches, media in object storage — so the fleet scales horizontally and redeploys many times a day.
- "Services" in the diagram are domains (Upload, Stories, Like & Comment, Feed), largely modules in the monolith with clear ownership of tables and events.
- Separate systems exist where runtime or scale differs: messaging connections, media pipelines, search, notification delivery, ML ranking, and Meta-wide graph and cache infrastructure.
- Async events glue domains together (
PostCreated,StoryCreated,LikeCreated,FollowCreated) — choreography made reliable by outbox-style publishing and idempotent consumers.
500M × 50 / 86,400 ≈ 290k requests/s average, maybe ~600k–900k/s at peak. If one stateless Python web server handles ~300 requests/s, peak needs roughly 2,000–3,000 servers for the web tier alone, plus headroom (N+1 per region, failover capacity). Because the tier is stateless, adding 20% capacity before a big event is just "deploy 500 more identical instances". Use back-of-the-envelope estimation to redo this with your own assumptions.15. Trade-offs & common pitfalls
✅ Microservices give you
- Independent deploys and team autonomy
- Independent scaling and technology choices per workload
- Fault isolation (with timeouts, breakers and fallbacks)
- Clear data ownership and smaller codebases
- Stateless tiers that autoscale and deploy safely
⚠️ And cost you
- Network latency and partial failures on every call
- No cross-service ACID transactions or joins
- Eventual consistency between data copies
- Heavy operational tooling: discovery, tracing, mesh, CI/CD per service
- Harder debugging and integration testing
Common pitfalls
- Distributed monolith — services that must deploy together or share a database.
- Chatty synchronous chains — A calls B calls C calls D; latencies add up and availability multiplies down. Prefer parallel fan-out, batching APIs and async events.
- Dual writes without an outbox — "write DB, then publish" silently loses events on crashes.
- Non-idempotent consumers — at-least-once delivery means duplicates; a duplicate
LikeCreatedmust not double-count. - Hidden state in "stateless" services — files on local disk, in-memory rate-limit counters, sticky sessions relied on for correctness.
- Retry storms — every layer retrying 3× turns one slow call into 27; use retry budgets, backoff with jitter and deadlines.
16. Real-world technologies
| Category | Tools | When to use |
|---|---|---|
| Containers & orchestration | Docker/OCI, Kubernetes (EKS, GKE, AKS), Nomad | Running many stateless services with self-healing, scheduling and autoscaling |
| Service discovery / config | Consul, etcd, ZooKeeper, Kubernetes DNS, Eureka | Mapping service names to healthy instances; leader election; dynamic config |
| RPC frameworks | gRPC, Apache Thrift, Finagle, REST/OpenAPI | Typed, efficient service-to-service calls |
| Service mesh / proxies | Envoy, Istio, Linkerd, Consul Connect, Cilium | Uniform mTLS, retries, traffic splitting and telemetry across languages |
| Event bus | Apache Kafka, Pulsar, AWS SNS/SQS, Google Pub/Sub | Async events, choreography, CDC streams |
| CDC / outbox relay | Debezium, Kafka Connect, AWS DMS | Publishing outbox rows or DB changes to Kafka reliably |
| Workflow / saga orchestration | Temporal, Cadence, AWS Step Functions, Netflix Conductor, Camunda | Long-running, multi-step business processes with compensations |
| Progressive delivery | Argo Rollouts, Flagger, LaunchDarkly, Unleash | Canary analysis, blue/green, feature flags |
17. Interview corner
Would you design Instagram as microservices or a monolith?
I'd describe the system as bounded contexts — upload, stories, engagement, graph, feed, search, messaging, users/auth — each owning its data and publishing events. Whether each is a separate deployable depends on team size and scaling profile. I'd extract pieces with very different runtimes from day one (DM WebSocket gateways, media workers, search indexing, fan-out workers) and keep CRUD product logic in a well-modularised, stateless web tier until team scale forces a split — which mirrors Instagram's Django monolith plus separate messaging, media, search and graph systems. Non-negotiables: stateless request tier, data ownership, async events for side effects.
What makes a service stateless and why do you care?
No state another instance would need is kept on the instance: auth from a token, data in shared DBs/caches, files in object storage, background work in a queue. So the LB balances freely, autoscalers and failures add/kill instances without data loss, and capacity scales linearly. Local caches are fine if losing them only costs latency. Inherently stateful pieces (DBs, caches, WebSocket gateways) live in dedicated tiers with their own replication.
A user publishes a post. How do you guarantee followers' feeds, search and notifications all eventually reflect it?
Write the post row and a PostCreated event into an outbox table in the same local transaction. A relay (poller or Debezium CDC) publishes to Kafka keyed by post_id, at-least-once. Fan-out, search indexer and notifications consume independently with their own offsets and are idempotent (timeline ZADD is naturally idempotent; notifications dedupe on event_id). A down consumer catches up from the retained log. No 2PC; consistency within seconds.
Explain 2PC and why we usually avoid it between microservices.
A coordinator asks all participants to prepare (persist and lock), then commits if all vote yes. It's atomic but blocking (locks held across round trips; participants stuck in doubt if the coordinator dies), needs every participant up, costs extra round trips and fsyncs, and many stores/brokers don't support it. Between services we use sagas with idempotency and outbox events; 2PC belongs inside databases engineered for it (Spanner, CockroachDB).
Choreography or orchestration for a saga — how do you decide?
Choreography suits loosely coupled, few-step broadcast side effects (PostCreated → fan-out, search, notifications). When the flow is long, ordered and needs compensations, the implicit workflow becomes hard to reason about, so I'd use an orchestrator (Temporal/Step Functions or in-house) that persists progress, retries and compensates — e.g. account deletion across dozens of stores. Many systems use both.
How would you roll out a risky change to the feed ranking code?
Ship it dark behind a feature flag; enable for employees, then a 1% population; compare guardrail metrics (errors, p99, crashes) and product metrics against a control group (A/B test); ramp 5/25/50/100% with automatic halt on regression; keep the flag as a kill switch, then delete it to avoid flag debt. The binary itself goes out via canary deploys first.
18. Check your understanding
What is the difference between a modular monolith and microservices, in one sentence each?
A modular monolith enforces boundaries in code (module APIs, table ownership) but deploys and runs as one unit with in-process calls. Microservices enforce boundaries with the network and separate databases, so each can be deployed and scaled independently — at the price of network failures and eventual consistency.
A request synchronously depends on 6 services, each 99.95% available. What is the best-case availability of the request?
0.99956 ≈ 0.9970, i.e. about 99.7% — roughly 2.2 hours of failure per month instead of ~22 minutes for one service. This is why we cut synchronous dependencies, add fallbacks and push side effects to async events.
An engineer stores uploaded photos on the web server's local disk "temporarily" before processing. Why is this a problem?
It makes the server stateful: the processing step must run on the same machine, the file is lost if the instance is terminated by autoscaling or a deploy, and the load balancer can't freely route retries. Uploads should go straight to object storage (pre-signed URLs) and processing should be triggered by an event.
The Post service commits the DB transaction, then crashes before publishing PostCreated. What happens and how do you fix it?
The post exists but no fan-out, indexing or notifications ever happen — a silent inconsistency. Fix with the transactional outbox: insert the event into an outbox table in the same transaction; a relay publishes it later (at-least-once), with idempotent consumers.
HPA: 25 pods run at 80% CPU with a 50% target. How many pods will the autoscaler request?
ceil(25 × 80 / 50) = ceil(40) = 40 pods. (In practice scale-up is also limited by rate policies and by whether the cluster autoscaler can add nodes quickly enough.)
Scenario: a saga increments the author's post_count, then moderation rejects the post. What should happen, and what properties must the compensation have?
Run compensating actions: mark the post removed, decrement post_count, emit PostRemoved so timelines and search drop it, delete renditions, notify the author. Compensations must be idempotent (safe to retry), eventually always succeed (retried until done) and be semantic undos — other users may already have seen the post, which is the isolation trade-off sagas accept.
Why does Conway's law suggest a 12-person startup should not run 40 microservices?
Architecture mirrors communication structure. With one or two teams, 40 services means each person owns several deploy pipelines, dashboards and on-call rotations, and almost every feature crosses many services owned by the same people — all the coordination overhead with none of the autonomy benefit. A modular monolith fits that organisation far better.
- Monolith, modular monolith and microservices are packaging choices; scale comes mostly from a stateless request tier plus a well-sharded data layer and async workers.
- Instagram's web tier is famously a large Django monolith; read the diagram's services as domains, with truly separate systems where runtimes differ (DMs, media, search, graph infrastructure).
- Statelessness pushes state into DBs, caches, session/token stores and object storage, enabling autoscaling, cheap failures and continuous deploys.
- Use sync RPC for queries the user waits on, async events for side effects; keep chains shallow and always use timeouts.
- Each service owns its data; replace cross-service transactions with sagas and make event publishing reliable with the transactional outbox and idempotent consumers.
- Operate with containers, Kubernetes, autoscaling, canaries and feature flags; split only when team structure and workload differences justify it (Conway's law, strangler fig).
Related lessons
- API Design — REST vs gRPC, idempotency keys and versioning for service APIs
- Message Queues & Kafka — the event bus behind choreography and the outbox
- Load Balancing — why stateless instances make balancing simple; sticky sessions
- API Gateway — the front door that routes to services and composes responses
- Resilience Patterns — timeouts, retries, circuit breakers between services
- Observability — distributed tracing across many services
- Databases — choosing a store per service
- CAP & Consistency — why cross-service data is eventually consistent
- Media Upload — a full saga-like flow with a post state machine and outbox