SSourav Saha
HomeExperienceSoftware DesignSystem DesignLearningBooksToolsContact
SSourav Saha

Building scalable backend systems, distributed infrastructure, and cloud-native applications.

Navigation

  • Home
  • Experience
  • Software Design
  • System Design
  • Learning

More

  • Books
  • Tools
  • Contact

Connect

  • LinkedIn
  • Email

© 2026 Sourav Saha. All rights reserved.

Built with using Next.js

System DesignService Mesh
System Designadvanced

Service Mesh

Learn how Service Meshes like Istio and Linkerd abstract away cross-cutting concerns (mTLS, retries, circuit breaking) from application code using sidecar proxies.

June 1, 202412 min read
service-meshistioenvoysidecarmicroservicesbuilding-blocktier-5

Metadata

FieldValue
Slugservice-mesh
DifficultyAdvanced
Estimated Reading Time15 min
Estimated Coding Time15 min
Tier5 — Production Infrastructure
Implementation LanguageGo (Conceptual)
SEO DescriptionLearn Service Meshes in system design. Understand Sidecar Proxies, Istio, Envoy, mTLS, and how they handle retries, circuit breaking, and observability transparently.

1. Overview

What problem does it solve?

In a microservices architecture, every service needs to implement the same list of cross-cutting concerns:

  • Retries & Timeouts (029 Retry & Backoff)
  • Circuit Breaking (028 Circuit Breaker)
  • Mutual TLS Encryption (mTLS)
  • Distributed Tracing (034 Distributed Tracing)
  • Rate Limiting (010 Rate Limiter)

If you have 50 microservices written in Java, Python, Go, and Node.js, you would need to implement all of this logic in every language, in every service. That's hundreds of thousands of lines of duplicated infrastructure code.

A Service Mesh pulls all of these cross-cutting concerns out of the application code and into a transparent network proxy layer (the Sidecar Proxy). The application code only contains business logic. The proxy handles everything else.

What breaks without it?

  • Polyglot Pain: Your Java services use Resilience4j for circuit breaking. Your Python services use tenacity. Your Go services use a custom library. They all behave slightly differently. Good luck debugging a cascading failure.
  • Security Gaps: Without centralized mTLS, services communicate over plain HTTP. Any compromised container can sniff traffic and steal credentials.

2. Motivation

When companies like Lyft scaled from a monolith to hundreds of microservices, they discovered that engineers were spending more time writing networking boilerplate (retries, tracing, encryption) than actual business features.

Lyft's answer was Envoy Proxy (open-sourced in 2016), a high-performance C++ network proxy that could be attached to every service as a "sidecar." Google then built Istio, a control plane that manages fleets of Envoy sidecars, creating the modern Service Mesh.


3. Real-World Usage

SystemUse Case
IstioThe most feature-rich service mesh. Uses Envoy as its data plane proxy.
LinkerdA lightweight, Rust-based service mesh focused on simplicity and performance.
AWS App MeshAmazon's managed service mesh for ECS and EKS workloads.
Consul Connect (HashiCorp)Combines service discovery with service mesh networking.

4. Prerequisites

ConceptBlock
Circuit Breaker028 Circuit Breaker
Distributed Tracing034 Distributed Tracing

5. Visual Explanation

Before: Library-Based Networking

Every service bakes its own networking logic into the app.

%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#ffcc00', 'edgeLabelBackground':'#ffffff'}}}%%
graph LR
    classDef svc fill:#cce5ff,stroke:#007bff,stroke-width:2px;

    subgraph Service A
        A_App["App Code +<br/>Retry Logic +<br/>Circuit Breaker +<br/>mTLS Client"]:::svc
    end

    subgraph Service B
        B_App["App Code +<br/>Retry Logic +<br/>Circuit Breaker +<br/>mTLS Server"]:::svc
    end

    A_App -- "Direct HTTP" --> B_App

After: Service Mesh (Sidecar Pattern)

The app knows nothing about networking. The Sidecar handles everything.

%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#ffcc00', 'edgeLabelBackground':'#ffffff'}}}%%
graph LR
    classDef svc fill:#cce5ff,stroke:#007bff,stroke-width:2px;
    classDef proxy fill:#d4edda,stroke:#28a745,stroke-width:2px;
    classDef control fill:#f8d7da,stroke:#dc3545,stroke-width:2px;

    subgraph Pod A
        A_App[App Code<br/>Business Logic ONLY]:::svc --> A_Proxy[Envoy Sidecar]:::proxy
    end

    subgraph Pod B
        B_Proxy[Envoy Sidecar]:::proxy --> B_App[App Code<br/>Business Logic ONLY]:::svc
    end

    A_Proxy -- "mTLS + Retry + Tracing" --> B_Proxy

    CP[Istio Control Plane]:::control -.-> |"Push Config"| A_Proxy
    CP -.-> |"Push Config"| B_Proxy

6. Internal Working

The Sidecar Proxy

In Kubernetes, every Pod can contain multiple containers. A Service Mesh automatically injects a second container (the Sidecar, e.g., Envoy) into every Pod.

When Service A sends an HTTP request to http://service-b:8080, the OS-level networking (iptables) transparently redirects the packet to the local Envoy sidecar (running on localhost:15001). Service A doesn't even know this happened.

The Envoy sidecar then:

  1. Encrypts the traffic with mTLS.
  2. Adds a traceparent header for distributed tracing.
  3. Applies retry and circuit breaker policies.
  4. Load Balances across the healthy instances of Service B.
  5. Exports metrics (Latency, Error Rate) to Prometheus.

The Control Plane vs Data Plane

  • Data Plane: The fleet of thousands of Envoy sidecar proxies that actually carry the network traffic.
  • Control Plane (Istio): The central brain that pushes configuration to all Envoy proxies simultaneously. E.g., "Set the timeout for calls to payment-service to 3 seconds." You change one config file, and Istio propagates it to every pod in the cluster within seconds.

7. Implementation

Why Go (Conceptual)? A real Envoy proxy is written in C++. Instead, we model the concept of the sidecar intercepting traffic. We build a Go HTTP reverse proxy that sits between two mock services, demonstrating how it transparently adds mTLS headers, tracing headers, and retry logic without the application knowing.

/*
035 - Service Mesh (Sidecar Proxy Simulation)
Demonstrates a Go HTTP proxy that intercepts traffic between two
mock services, adding retries, tracing headers, and logging
transparently—mimicking what Envoy does in a service mesh.
*/
package main

import (
	"fmt"
	"io"
	"math/rand"
	"net/http"
	"strings"
	"time"
)

// ── 1. The "Upstream" Service B (Fragile) ──

func startServiceB() {
	mux := http.NewServeMux()
	mux.HandleFunc("/api/data", func(w http.ResponseWriter, r *http.Request) {
		// Simulate 50% failure rate
		if rand.Float32() < 0.5 {
			http.Error(w, "500 Internal Server Error", http.StatusInternalServerError)
			return
		}
		traceID := r.Header.Get("X-Trace-Id")
		fmt.Fprintf(w, `{"data": "Hello from Service B!", "trace_id": "%s"}`, traceID)
	})
	http.ListenAndServe(":8082", mux)
}

// ── 2. The Sidecar Proxy (Envoy Simulation) ──

func sidecarProxy(w http.ResponseWriter, r *http.Request) {
	maxRetries := 3
	
	for attempt := 1; attempt <= maxRetries; attempt++ {
		// Inject Tracing Header (Context Propagation)
		traceID := fmt.Sprintf("trace-%d", time.Now().UnixNano())
		
		// Create upstream request
		upstreamURL := "http://localhost:8082" + r.URL.Path
		req, _ := http.NewRequest(r.Method, upstreamURL, r.Body)
		
		// Copy original headers, then add mesh headers
		for k, v := range r.Header {
			req.Header[k] = v
		}
		req.Header.Set("X-Trace-Id", traceID)
		req.Header.Set("X-Mesh-Encrypted", "mTLS-v1.3")
		
		fmt.Printf("[Sidecar] Attempt %d/%d -> %s (Trace: %s)\n", attempt, maxRetries, upstreamURL, traceID[:20])
		
		// Send to upstream Service B
		client := &http.Client{Timeout: 2 * time.Second}
		resp, err := client.Do(req)
		
		if err != nil || resp.StatusCode >= 500 {
			fmt.Printf("[Sidecar] ⚠️  Attempt %d failed. Retrying...\n", attempt)
			if resp != nil {
				resp.Body.Close()
			}
			time.Sleep(100 * time.Millisecond) // Backoff
			continue
		}
		
		// Success! Forward the response back to the caller.
		defer resp.Body.Close()
		body, _ := io.ReadAll(resp.Body)
		fmt.Printf("[Sidecar] ✅ Success on attempt %d.\n", attempt)
		w.Header().Set("Content-Type", "application/json")
		w.Write(body)
		return
	}
	
	// Circuit Breaker: All retries exhausted
	fmt.Println("[Sidecar] ❌ All retries exhausted. Returning 503.")
	http.Error(w, "Service Unavailable", http.StatusServiceUnavailable)
}

func startSidecar() {
	mux := http.NewServeMux()
	mux.HandleFunc("/", sidecarProxy)
	fmt.Println("🚀 Sidecar Proxy running on :8081 (intercepting traffic to Service B on :8082)")
	http.ListenAndServe(":8081", mux)
}

// ── 3. Service A (The Application — Pure Business Logic) ──

func serviceAHandler(w http.ResponseWriter, r *http.Request) {
	// Service A simply calls "Service B" via its sidecar.
	// It knows NOTHING about retries, tracing, or mTLS.
	resp, err := http.Get("http://localhost:8081/api/data")
	if err != nil {
		http.Error(w, "Failed to reach Service B", 500)
		return
	}
	defer resp.Body.Close()
	
	body, _ := io.ReadAll(resp.Body)
	fmt.Fprintf(w, "Service A received: %s", string(body))
}

// ── Test Harness ──

func main() {
	rand.Seed(time.Now().UnixNano())
	
	go startServiceB()   // Start the fragile upstream service
	go startSidecar()    // Start the sidecar proxy
	
	time.Sleep(200 * time.Millisecond)
	
	// Simulate Service A making a request
	fmt.Println("\n--- Service A calls Service B via Sidecar ---")
	resp, _ := http.Get("http://localhost:8081/api/data")
	if resp != nil {
		body, _ := io.ReadAll(resp.Body)
		resp.Body.Close()
		fmt.Printf("\nFinal Response: %s (Status: %d)\n", strings.TrimSpace(string(body)), resp.StatusCode)
	}
}

Sample Output

🚀 Sidecar Proxy running on :8081 (intercepting traffic to Service B on :8082)

--- Service A calls Service B via Sidecar ---
[Sidecar] Attempt 1/3 -> http://localhost:8082/api/data (Trace: trace-17129456781)
[Sidecar] ⚠️  Attempt 1 failed. Retrying...
[Sidecar] Attempt 2/3 -> http://localhost:8082/api/data (Trace: trace-17129456782)
[Sidecar] ✅ Success on attempt 2.

Final Response: {"data": "Hello from Service B!", "trace_id": "trace-17129456782"} (Status: 200)

Service A has zero knowledge of retries, tracing, or encryption. The Sidecar handled everything transparently.


8. Complexity

MetricDetails
Latency OverheadEach hop through an Envoy sidecar adds ~1-3ms of latency. For a 10-service call chain, this is 10-30ms of added overhead.
Memory OverheadEach Envoy sidecar consumes ~50-100MB of RAM. At 1,000 pods, that's ~50-100GB of RAM just for proxies.

9. Trade-offs

SetupProsCons
Library-Based (Hystrix, Resilience4j)No extra infrastructure. No latency overhead. Fine-grained, business-aware fallbacks.Language-specific. Duplicated across services. Hard to enforce uniform policies.
Service Mesh (Istio/Linkerd)Language-agnostic. Zero code changes. Centralized policy management. mTLS for free.Significant operational complexity. Latency overhead. High memory footprint. Hard to debug networking issues.

10. Production Evolution

FeatureThis ImplementationProduction (Istio + Envoy)
Traffic RoutingStaticIstio enables advanced traffic management: Canary Deployments (route 5% of traffic to v2 of a service), A/B Testing (route users with header x-user: beta to the new service), and Traffic Mirroring (shadow production traffic to a staging environment for testing).
SecurityFake headermTLS is automatically negotiated between all sidecars. No application developer ever touches a TLS certificate. Istio's Citadel component automatically issues and rotates short-lived certificates.
InjectionManualIn Kubernetes, Istio uses a Mutating Admission Webhook. When a new Pod is created, the webhook automatically injects the Envoy sidecar container before the pod starts. The developer never writes a single line of mesh configuration in their Deployment YAML.

11. Common Bugs

BugWhat happensFix
Sidecar Not ReadyThe application container starts faster than the Envoy sidecar. The first few requests fail because the sidecar isn't listening yet.Use holdApplicationUntilProxyStarts in Istio to prevent the app container from starting before the proxy is ready.
Retry + Retry = Retry StormThe sidecar retries 3 times. The application code ALSO retries 3 times. That's 9 total requests to the struggling upstream service.If you use a service mesh, REMOVE all retry logic from your application code. Let the mesh handle it.
Debugging Black HoleA request fails. The developer looks at application logs and sees nothing. The failure happened inside the Envoy proxy, which logs to a different stdout stream.Ensure Envoy access logs are shipped to the same centralized logging system (ELK) and are easily correlated via Trace IDs.

12. Interview Questions

  1. What is the Sidecar Pattern? Hint: Deploying a helper process (like Envoy) alongside every application instance. The sidecar intercepts all network traffic transparently and handles cross-cutting concerns like retries, encryption, and tracing.

  2. What is the difference between the Data Plane and the Control Plane in a Service Mesh? Hint: The Data Plane is the fleet of sidecar proxies (Envoy) that carry the actual network traffic. The Control Plane (Istio) is the centralized management layer that pushes configuration (retry policies, routing rules) to all sidecars.

  3. When should you NOT use a Service Mesh? Hint: If you have fewer than ~10 services, the operational complexity of a mesh is not justified. Also, latency-sensitive applications (like high-frequency trading) cannot tolerate the extra 1-3ms per hop.


13. Used By (Downstream Blocks)

  • 036 Reverse Proxy & API Gateway — An API Gateway sits at the edge of the network. A Service Mesh sits between internal services. They are complementary.

14. Used In (Case Studies)

SystemUse Case
LyftCreated Envoy because their microservice network was impossible to manage with per-language libraries.
AirbnbUses Envoy and Istio for inter-service mTLS, traffic shifting during deployments, and centralized rate limiting.
eBayUses a custom service mesh to manage traffic between tens of thousands of microservice instances.

15. Related Blocks

RelationshipBlock
Previous028 Circuit Breaker
Next036 Reverse Proxy & API Gateway

16. Try It Yourself

Exercise 1: Circuit Breaker in the Sidecar

Add a counter to the sidecarProxy function. If the upstream service has returned 500 errors for the last 5 consecutive requests, the sidecar should stop forwarding requests entirely and instantly return 503 Service Unavailable for the next 10 seconds (the OPEN state).

Exercise 2: Canary Routing

Modify the sidecar so that if the incoming request has a header X-Canary: true, it forwards the request to http://localhost:8083 (a "v2" of Service B) instead of :8082.


Website Metadata

FieldValue
Hero TitleService Mesh
Hero SubtitleHow to move retries, encryption, and tracing out of your code and into the network itself.
BreadcrumbSystem Design → Building Blocks → Service Mesh
Sidebar CategoryTier 5 — Production Infrastructure
Search Keywordsservice mesh, istio, envoy, sidecar, mtls, microservices, linkerd, data plane, control plane
Internal Links← 028 Circuit Breaker · → 036 Reverse Proxy
Suggested IllustrationA city road with many cars (requests). Instead of each car having its own GPS, traffic lights, and radio, a transparent guardian angel (sidecar) sits in the passenger seat of every car, handling navigation and safety invisibly.
Suggested AnimationService A fires a raw HTTP request. The request enters a small shield (Sidecar). The shield adds a lock (mTLS), a tag (Trace ID), and a retry badge. The armored request travels to Service B's sidecar, which strips the armor and delivers the clean request.
PreviousReverse Proxy (Nginx, Envoy, HAProxy)NextSSTable & LSM Tree