Skip to Content
Course content

223: Building a Simple Service Mesh Sidecar Concept

Click on the "Edit" button in the top corner of the screen to edit your slide content.

When we talk about "service meshes," it's easy to get lost in the marketing jargon of Istio or Linkerd. But at its core, a service mesh is just a pattern to solve a recurring nightmare: how do you handle cross-cutting concerns—like retries, timeouts, and circuit breaking—without polluting every single one of your microservices with the same boilerplate code?

Imagine we're building a PaymentService in Go. This service needs to talk to an external BankAPI. Now, the Bank API is flaky. To keep our system stable, we need to implement a retry policy with exponential backoff and a strict 2-second timeout. I've seen a lot of engineers start this the "obvious" way.

The temptation of the internal wrapper

The naive approach is to build a robust internal client library. You'd write a BankClient struct in Go, implement the retry logic using a loop and a timer, and then distribute this as a shared module across your organization. It looks clean at first. Your business logic just calls client.SendPayment(), and the library handles the messiness under the hood.

// The "Library" approach (Naive)
func (c *BankClient) SendPayment(req PaymentRequest) error {
    return retry.Do(func() error {
        ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
        defer cancel()
        return c.httpClient.Do(ctx, req)
    }, retry.Attempts(3), retry.Delay(time.Second))
}


Here is where this breaks. The moment your company decides to introduce a ShippingService written in Rust or a NotificationService in Node.js, your Go library is useless. You now have to rewrite that exact same retry and timeout logic in two other languages. Worse, if you realize the 2-second timeout is too aggressive and needs to be 5 seconds, you have to update the library, create a new version, and convince every single team to bump their dependency and re-deploy their entire service. It's a coordination disaster.

Moving the complexity to a sidecar

The better way—and the fundamental concept of a service mesh—is to move that logic out of the application process entirely. Instead of a library, we build a small, lightweight proxy in Go that runs as a "sidecar" container in the same pod or on the same host as the PaymentService.

In this model, the PaymentService doesn't know the Bank API exists. It just sends a plain HTTP request to localhost:8080. The sidecar, listening on that port, intercepts the request, applies the retry logic, manages the timeouts, and then forwards the request to the actual Bank API. I like this because it turns a code problem into a configuration problem.

// A simplified conceptual sidecar handler in Go
func sidecarHandler(w http.ResponseWriter, r *http.Request) {
    // The sidecar handles the "infrastructure" concerns
    var resp *http.Response
    var err error

    for i := 0; i < 3; i++ {
        ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
        resp, err = http.DefaultClient.Do(r.WithContext(ctx))
        cancel()
        
        if err == nil && resp.StatusCode < 500 {
            break
        }
        time.Sleep(time.Duration(i+1) * time.Second)
    }
    
    // Forward the final result back to the app
    // (Simplified for brevity)
    w.WriteHeader(resp.StatusCode)
    io.Copy(w, resp.Body)
}


Why we trade milliseconds for sanity

Now, if you're a performance purist, you'll immediately point out that I've just added an extra network hop. The request now goes App -> Sidecar -> Network -> API instead of App -> Network -> API. You're adding a few milliseconds of latency. In 99% of business applications, that trade-off is a bargain.

By decoupling the communication policy from the business logic, you gain the ability to change how your entire system behaves without touching a single line of application code. Want to implement mTLS for security? Update the sidecar. Want to shift 10% of traffic to a canary version of the Bank API? Update the sidecar. Your Go developers can get back to writing payment logic instead of debugging TCP timeouts and retry loops.




📋 Practical Task

Build a Resilient Request Proxy Sidecar

Your task is to implement a functional sidecar proxy in Go. Create a program that listens on :8080 and proxies all incoming requests to a target URL (you can use https://httpbin.org/status/500 to simulate a failing server).

Your proxy must implement the following "mesh" features:

  • Automatic Retries: If the target server returns a 5xx error, the proxy should retry the request up to 3 times before giving up.
  • Hard Timeout: Each individual attempt to the target server must be cancelled if it takes longer than 1 second.
  • Logging: The proxy should print to the console every time a retry is attempted, including the attempt number.

Test your implementation by sending a request to your proxy using curl localhost:8080 and verifying that it attempts to hit the failing endpoint multiple times before finally returning the error to you.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.