Advanced API Gateway and Service Mesh
Once an API stops being "one server answering requests" and becomes a dozen services calling each other, two new problems show up. First, clients need a single, stable front door instead of having to know about every internal service. Second, the services themselves need consistent rules for retries, timeouts, encryption, and observability without every team reinventing them. The first problem is solved by an API gateway. The second is solved by a service mesh. They look similar from a distance — both sit "in front of" traffic and both talk about routing, TLS, and rate limiting — but they operate at different layers of your architecture, and understanding the difference will save you from reaching for the wrong tool.
The gateway: one door, many rooms
An API gateway sits at the edge of your system, between the public internet and your internal services. Every external request enters through it. The gateway's job is to look at the request and decide: which backend service handles this, does the caller have a valid token, has this client exceeded its rate limit, and what should the response look like once it comes back.
Popular gateways like Kong, Ambassador, and Envoy (used standalone) all do roughly the same thing: match incoming requests to routes, apply a chain of policies (auth, rate limiting, transformation), and proxy to an upstream. You can build a serviceable version of this yourself in Go using nothing but the standard library's net/http/httputil:
package main
import (
"log"
"net/http"
"net/http/httputil"
"net/url"
)
func newProxy(target string) *httputil.ReverseProxy {
u, err := url.Parse(target)
if err != nil {
log.Fatal(err)
}
proxy := httputil.NewSingleHostReverseProxy(u)
originalDirector := proxy.Director
proxy.Director = func(r *http.Request) {
originalDirector(r)
r.Header.Set("X-Forwarded-Gateway", "go-gateway")
}
return proxy
}
func main() {
mux := http.NewServeMux()
mux.Handle("/users/", newProxy("http://users-service:8081"))
mux.Handle("/orders/", newProxy("http://orders-service:8082"))
log.Fatal(http.ListenAndServe(":8080", mux))
}
This is a real, working reverse proxy: httputil.ReverseProxy rewrites the request's scheme and host, forwards it, and streams the response back — including chunked and streamed bodies. Production gateways add far more (dynamic route discovery, plugin pipelines, gRPC support), but the core mechanism is exactly this.
Cross-cutting policies at the edge
Because every request passes through the gateway, it is the natural place to enforce policies that would otherwise be duplicated across every service: authentication, request/response logging, and circuit breaking. Chapter 6.18 already introduced github.com/sony/gobreaker/v2 for protecting a single call to a downstream dependency; the gateway is simply the natural place to wire one up per backend service, since every request to that service already funnels through this one choke point:
import "github.com/sony/gobreaker/v2"
var ordersBreaker = gobreaker.NewCircuitBreaker[*http.Response](gobreaker.Settings{
Name: "orders-service",
MaxRequests: 5,
Timeout: 10 * time.Second,
})
func callOrders(req *http.Request) (*http.Response, error) {
return ordersBreaker.Execute(func() (*http.Response, error) {
return http.DefaultClient.Do(req)
})
}
One breaker per backend, keyed by Name, means a struggling orders-service trips its own breaker and fails fast without affecting calls the gateway proxies to users-service or any other backend — exactly the isolation you want when only one downstream dependency is having a bad day.
The mesh: every service gets a bodyguard
A service mesh solves a different problem: traffic between your internal services, not traffic from the outside world. In a mesh like Istio or Linkerd, every service instance gets a sidecar proxy (usually Envoy) injected next to it. Your Go service talks to localhost, the sidecar intercepts that traffic, and the sidecar handles mutual TLS, retries, load balancing, and telemetry — all without your application code knowing anything happened.
The appeal of the sidecar model is that your Go code stays simple. You don't import a mesh SDK; you just make a normal HTTP or gRPC call to another service's address, and the sidecar intercepts it transparently at the network layer (via iptables rules in the pod, in Kubernetes-based meshes). This is a deliberate architectural trade-off: it moves cross-cutting network concerns out of application code and into infrastructure, at the cost of an extra network hop and additional operational complexity (certificate rotation, sidecar upgrades, mesh control-plane failures).
When you don't have a mesh yet
Not every team runs Istio or Linkerd — plenty of Go services implement mesh-like behavior directly in a shared HTTP client. A lightweight version of "retries plus timeouts" without any external dependency:
func resilientGet(ctx context.Context, url string) (*http.Response, error) {
client := &http.Client{Timeout: 2 * time.Second}
var lastErr error
for attempt := 0; attempt < 3; attempt++ {
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
resp, err := client.Do(req)
if err == nil && resp.StatusCode < 500 {
return resp, nil
}
lastErr = err
time.Sleep(time.Duration(attempt+1) * 100 * time.Millisecond)
}
return nil, lastErr
}
This is a reasonable starting point for a small system. Once you have dozens of services and multiple teams, the operational cost of every team writing (and forgetting to update) this same logic usually justifies moving to a real mesh.
Choosing between them
A gateway answers "who gets in and how do we treat external traffic"; a mesh answers "how do our own services talk to each other safely and reliably."
Most systems that operate at meaningful scale end up with both: a gateway (or several, per API surface) at the edge, and a mesh handling internal service-to-service traffic. As a Go developer, the practical takeaway is that neither one requires you to embed a vendor SDK into your business logic. Gateways are usually configured declaratively (routes, plugins) outside your service; meshes are usually transparent at the network layer. Your Go code's job is to expose a clean HTTP or gRPC interface, respect context cancellation and deadlines passed down to it, and emit the metrics and trace headers that let the surrounding infrastructure do its job well.
Frequently Asked Questions
Isn't a service mesh just a gateway that lives deeper inside the system? It's tempting to think of them as the same idea at different scales, but they answer different questions about traffic direction. A gateway handles north-south traffic, the requests arriving from outside your system, while a mesh handles east-west traffic between your own services, and a call from one internal service to another never passes through the edge gateway at all.
Why reach for sony/gobreaker instead of writing my own retry-and-give-up loop?
A hand-rolled circuit breaker is easy to get subtly wrong: forgetting the half-open probing state, or never resetting after recovery, so it just stays open forever. gobreaker already implements the closed, open, and half-open states correctly, tripping after consecutive failures and probing with a limited number of requests before fully reopening, which is exactly the behavior that protects a struggling downstream service without permanently locking it out.
My mesh sidecar retries failed calls, and my Go service also retries the same call — is that a problem? Yes, and it is one of the most common self-inflicted outages in a meshed system. If the sidecar retries three times and your application code retries three more times on top of that, a single slow downstream call can multiply into nine attempts, turning a minor blip into a pile-up. Check what your mesh already guarantees before adding a competing resilience layer in application code.
Where should authentication live, at the gateway or inside each service? The gateway is the natural place for coarse-grained checks like "does this caller have a valid token at all," since every external request already passes through it. Finer-grained authorization, such as "can this specific user modify this specific order," usually still belongs in the service itself, because the gateway does not know your domain's business rules.