TL;DR

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:

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.

🧠 Analogy A monolith is a food truck: one cook, one small kitchen — fast and easy to coordinate until the queue gets enormous. Microservices are a restaurant kitchen with stations (grill, pastry, salads), each with its own chef and fridge; you can add a grill chef without hiring a pastry chef. But now you need tickets (messages), a pass where dishes are assembled (API composition), a head chef calling orders (orchestration), and rules for when the steak burns after the salad is plated (compensation). A modular monolith is a well-organised food truck: one kitchen, every tool in its labelled place.

2. Monolith vs modular monolith vs microservices

2.1 Definitions

Monolith Modular monolith Microservices users posts feed likes code freely calls code; any module reads any table 1 deployable One shared DB all tables, all modules users posts feed likes calls only via module APIs (in-process, no network) 1 deployable userspostsfeedlikes one DB, schema per module User svc Post svc Feed svc Like svc Postgres Cassandra Redis Cassandra Kafka (events) N deployables, DB per service
Same four domains, three packaging choices. The difference is where the boundaries are enforced: nowhere (monolith), by code rules (modular monolith), or by the network and separate databases (microservices).

2.2 Side-by-side comparison

DimensionMonolithModular monolithMicroservices
Deployable units11Tens to thousands
Calls between featuresIn-process function calls (ns–µs)In-process calls through module interfacesNetwork calls (≈0.5–5 ms each), can fail or time out
DataOne shared schema; joins everywhereOne DB, but tables owned per module; cross-module joins discouragedDatabase per service; no cross-service joins
TransactionsEasy: one ACID transactionEasy (same DB), but discouraged across modulesNo cross-service ACID; use sagas, outbox, idempotency
ScalingScale everything togetherScale everything together (can route endpoint pools separately)Scale each service independently
Team autonomyLow — shared release trainMedium — clear ownership, shared releaseHigh — independent releases
Failure isolationPoor — one bad path can crash the processPoor to mediumGood, if timeouts and circuit breakers are used
Operational costLowestLowHigh: discovery, tracing, mesh, many pipelines
Best forStart-ups, small teams, unclear domainsMost companies, most of the timeLarge orgs with many teams and very different scaling needs
📸 Honest note about Instagram Instagram's server began as a Django (Python) monolith, and Instagram engineers have publicly described it — in talks and blog posts about their Python 3 migration, static analysis tooling and the Cinder CPython fork — as a monolith with millions of lines of Python, deployed continuously many times a day. It is reasonably described as one of the largest Django deployments in the world. So when the architecture diagram on the home page shows "Upload Service", "Stories Service", "Like & Comment Service" and so on, read them as logical components (domains). Some genuinely are separate systems — real-time messaging, media processing, search indexing, notifications and much of Meta's shared infrastructure (e.g. the TAO graph store and caches) run as separate services — while many product features are modules inside the big web tier. That hybrid is extremely common in real companies, and it is a perfectly good answer in an interview as long as you can explain the trade-offs.

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)

⚠️ The distributed monolith The worst outcome: services split over the network that still must deploy together, share a database, or call each other in long synchronous chains — all the costs, none of the benefits. Symptom: "we need to release A, B and C together".

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

  1. Horizontal scaling is trivial. Need more capacity? Start 200 more identical instances and register them with the load balancer. No data needs to move.
  2. Any instance can serve any request. The load balancer can use simple round-robin or least-connections — no sticky sessions needed. Load spreads evenly.
  3. 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.
  4. 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:

📱 Clientstoken in header Load balancerround-robin App instance 1no local state App instance 2no local state App instance Nno local state Session / token storeRedis or signed JWT (no store) Cache (Redis / Memcached)timelines, counters, hot objects DatabasesPostgreSQL, Cassandra (truth) Object storage (S3)uploaded photos and videos
Stateless app instances are interchangeable. All durable or shared state lives below them, in stores built for it.
Kind of stateNaive (stateful) placeWhere it should liveInstagram example
Login sessionIn-memory session dict on one serverSigned 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 dataLocal SQLite or filesDatabases (source of truth)Users in sharded PostgreSQL; posts in Cassandra
Hot derived dataPer-process dictionariesShared cache (Redis/Memcached); small per-process caches are OK as an optimisation onlyFeed timelines and counters in Redis
Uploaded filesLocal disk of the server that received themObject storagePhotos uploaded straight to S3 via pre-signed URLs
Background workThreads inside the web processDurable queue/log + workersKafka → fan-out, media processing
Long-lived connections—A dedicated, deliberately stateful connection tier with a registry mapping user → gatewayDM WebSocket gateways
💡 Stateless ≠ no memory at all Stateless services can keep in-memory caches (a 30-second LRU of profiles) or warm connection pools — losing them must only cost performance, never correctness. Inherently stateful components (databases, caches, WebSocket gateways) are concentrated into a few well-managed tiers.

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:

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 discoveryServer-side discovery
Who chooses the instanceThe calling service, using a smart client libraryA load balancer / proxy in the middle
Extra network hopNone — direct instance-to-instanceYes (through the LB), unless the proxy runs as a local sidecar
Load-balancing smartsRich: least-outstanding-requests, power-of-two-choices, zone awarenessWhatever the LB supports
Language burdenEvery language needs the client library (Java, Python, Go…)Clients stay dumb; just call a hostname
ExamplesNetflix Eureka + Ribbon, gRPC client-side LB, FinagleAWS 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).

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)
CouplingTemporal: both must be up at onceDecoupled in time; broker buffers
User latencyAdds up along the chainFast "accepted"; work happens later
ConsistencyResult visible immediatelyEventual (seconds)
FailuresTimeouts, retries, circuit breakersRedelivery, dead-letter queues, idempotent consumers
Load spikesHit every downstream immediatelyQueue absorbs them (load levelling)
InstagramFeed read → post metadata, counts, "has viewer liked"PostCreated → fan-out, search, notifications; LikeCreated → notifications
💡 Rule of thumb Sync for queries the user waits on, with shallow chains (parallel fan-out, ≤2–3 hops). Async events for side effects of a write. "Post a photo" synchronously does only the minimum (validate, store metadata).

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?

  1. 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.
  2. Replicated read models (CQRS) — subscribe to others' events and keep a denormalised copy: Search builds Elasticsearch from UserUpdated/PostCreated and never queries the User DB.
  3. Denormalise into the event — PostCreated carries author_id, timestamp, media type and audience so consumers needn't call back.
⚠️ Price of ownership Copies are eventually consistent — a renamed user may show the old name in search for seconds. Every copy needs a clear source of truth and a way to be rebuilt (replay the log or re-scan the owner).

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):

  1. Post service: insert post row with status = PROCESSING (local transaction) → emit PostSubmitted.
  2. Media processing: generate renditions in S3 → emit MediaReady (or MediaFailed).
  3. Integrity / moderation: classify the media and caption → emit ContentApproved (or ContentRejected).
  4. Post service: set status = PUBLISHED; User service increments the author's post_count → emit PostCreated.
  5. 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 (events)Orchestration (central coordinator) Kafka topics Post svc Media workers Moderation Fan-out / search each service reacts to events and emits new ones Publish-postorchestrator Post svc Media workers Moderation User svc one component tells each step what to do
Choreography spreads the workflow logic across event handlers; orchestration puts it in one explicit state machine.
ChoreographyOrchestration
HowServices subscribe to each other's events and reactA coordinator (workflow engine) sends commands and tracks state
CouplingVery loose; easy to add a new consumerServices depend on the orchestrator's commands
VisibilityThe flow is implicit — hard to answer "where is post 42 stuck?"Explicit state per saga instance; easy to monitor and retry
RiskCyclic event chains, "event spaghetti"Orchestrator becomes a bottleneck or a "god service"
FitsSimple, few-step flows; broadcast side effects (fan-out, search, notifications)Long, multi-step, business-critical flows with compensations
ToolsKafka, SNS/SQS, any pub/subTemporal, Cadence, AWS Step Functions, Camunda, Netflix Conductor
📸 Instagram-flavoured orchestration: account deletion Deleting an account must remove posts, stories, comments, likes, follow edges, messages, search entries, caches and blobs across dozens of stores, verifiably and within legal deadlines — a classic orchestrated saga that walks the user's data graph, retries for days if needed and records progress. Meta has publicly described such a framework (the "DELF" paper, USENIX Security 2020). "A post was published", by contrast, fits choreography.

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?

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.

Post service1 local transaction
→posts + outbox rows
Post DBoutbox table
→poll or CDC
Outbox relayDebezium / poller
→publish
Kafkapost-events
→at-least-once
Consumersidempotent
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)

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

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

StrategyHow it worksProsCons
Rolling updateReplace instances in batches (e.g. 10% at a time) with the new versionNo extra capacity needed; default in KubernetesBoth versions serve traffic during rollout; rollback is another rolling update
Blue/greenRun the full new version ("green") beside the old ("blue"); flip the load balancer to green at once; keep blue for instant rollbackInstant switch and instant rollback; test green with real infra before cut-overNeeds 2× capacity during release; DB schema must work for both versions
CanarySend 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 regressionLimits blast radius; catches problems that only show under real trafficNeeds good metrics and automated analysis; slow for urgent fixes
Feature flagsShip code "dark", then enable it at runtime for specific users, % of users, countries or employeesDecouples deploy from release; instant kill switch; enables A/B testsFlag debt (stale flags), combinatorial testing, config itself can cause outages
📸 Instagram's continuous deployment Instagram engineering has publicly described continuous deployment of its Django monolith: every change that passes tests is pushed to production, many times a day, first to a small canary set of servers where automated checks watch error rates before rolling out to the whole fleet. Product changes are typically released via feature flags/gating (Meta's "Gatekeeper" system is publicly documented), so a new Stories feature can be turned on for employees, then 1% of users, then everyone — and turned off in seconds without a deploy. This is a big reason a single monolith can support so many engineers.
⚠️ Schema changes during deploys During any rolling or canary deploy, old and new code run at the same time against the same database and the same event topics. Use expand → migrate → contract: first add the new column/field (old code ignores it), deploy code that writes both and reads the new one, backfill, and only later remove the old column. Event schemas must be backward- and forward-compatible (add optional fields; never repurpose a field).

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.

Control plane (e.g. Istiod)config, certificates, endpoint lists Pod: feed-service Feed appcalls localhost Envoysidecar proxy Pod: graph-service Envoysidecar proxy Graph appplain HTTP/gRPC mTLS retries, timeouts, metrics, traces Data plane = all sidecars. The apps never deal with TLS, discovery or retries themselves.
A service mesh: each instance gets a sidecar proxy; a control plane pushes routing rules, endpoints and certificates to all proxies.

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:

13. When to split — and when not to

13.1 Good reasons to extract a service

13.2 Bad reasons

13.3 How to split safely: the strangler fig

  1. Modularise in place: define the module's API and stop other modules touching its tables.
  2. Route through a facade (gateway or in-code) and build the new service behind the same API; shadow traffic to compare responses.
  3. Migrate data: backfill, keep in sync via CDC, then move ownership.
  4. 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:

📱 Appauth token
→
LB + API gatewayTLS, auth, rate limits
→route by path
Stateless web tierDjango monolith: feed, likes, comments, upload API…
→RPC
Specialised servicesgraph store, DM gateways, search, ranking
Web tier writes+ outbox / events
→
Kafka-style event bus
→
Async workersfan-out, media, notifications, indexer, ML
→
Data layerPostgres, Cassandra, Redis/Memcache, S3, ES
📸 Rough numbers (assumptions) Assume 500M daily active users making ~50 API requests each per day → 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

16. Real-world technologies

CategoryToolsWhen to use
Containers & orchestrationDocker/OCI, Kubernetes (EKS, GKE, AKS), NomadRunning many stateless services with self-healing, scheduling and autoscaling
Service discovery / configConsul, etcd, ZooKeeper, Kubernetes DNS, EurekaMapping service names to healthy instances; leader election; dynamic config
RPC frameworksgRPC, Apache Thrift, Finagle, REST/OpenAPITyped, efficient service-to-service calls
Service mesh / proxiesEnvoy, Istio, Linkerd, Consul Connect, CiliumUniform mTLS, retries, traffic splitting and telemetry across languages
Event busApache Kafka, Pulsar, AWS SNS/SQS, Google Pub/SubAsync events, choreography, CDC streams
CDC / outbox relayDebezium, Kafka Connect, AWS DMSPublishing outbox rows or DB changes to Kafka reliably
Workflow / saga orchestrationTemporal, Cadence, AWS Step Functions, Netflix Conductor, CamundaLong-running, multi-step business processes with compensations
Progressive deliveryArgo Rollouts, Flagger, LaunchDarkly, UnleashCanary 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.

🎯 Key takeaways

Related lessons