Skip to content
Architecture

Designing a Production-Ready Go Backend: Architecture, Reliability and Operations

How the pieces of a production Go backend fit together: application architecture, reliability patterns, data, messaging, security, observability, and deployment — plus when to choose a monolith, modular monolith, or microservices.

GoBackend.dev16 min read
GoBackendArchitectureMicroservicesProductionSystem Design

The problem

"Use best practices" isn't actionable advice for a backend — it's a list of nouns (retries, caching, observability, Kubernetes) with no information about how they fit together, in what order, or which ones a given system actually needs. Most of the individual pieces are well understood in isolation. What's harder to find written down anywhere is the floor plan: where each piece sits relative to the others, and what actually breaks when one of them fails.

This article is that floor plan. It won't re-teach any single mechanism in depth — every section links to the dedicated article for that — but it will show you where each one lives in a real system, and what happens to the rest of the system when it doesn't work.

Why it matters

A backend built by adding practices one at a time, without a picture of how they compose, tends to end up with redundant protection in some places (three overlapping timeouts) and none in others (a single dependency with no circuit breaker that takes the whole system down when it degrades). The value of seeing the whole system at once is knowing which gaps actually matter for this architecture, not a generic checklist.

The system

The example running through this article is an order-processing platform — concrete enough to reason about, ordinary enough that the same shape applies to most backends that take a request, persist something, and tell other parts of the system about it.

Rendering diagram…

An order comes in through the load balancer to the Go API, which authenticates the caller, checks its rate limit, validates the request, and runs the actual business logic — reading and writing PostgreSQL, reading (and sometimes writing) Redis as a cache, and writing an event into the outbox in the same transaction as the order itself. A worker relays outbox events to the broker; the Worker Service and Notification Service consume them independently. Every section below is a piece of this one diagram.

Application architecture

The API box in the diagram is itself layered: handlers decode HTTP and encode responses, services hold business logic and depend only on interfaces they define themselves, and repositories are the only code that knows SQL exists. Dependencies point inward, toward the service layer, never outward toward infrastructure. This shape doesn't change as the rest of the diagram grows — adding Redis, an outbox, or a second downstream service adds new interfaces the service layer depends on, not a different layering scheme. The full treatment, including where the line goes too far into unnecessary abstraction, is in Building a Go API with Clean Architecture Without Overengineering.

Reliability

Every arrow leaving the API box to something that can be slow or unavailable — PostgreSQL, Redis, the broker, any third-party call a notification might make — needs a bounded timeout, and calls worth retrying need backoff with jitter so a struggling dependency doesn't get hit harder by retries at the exact moment it's least able to handle them. See Retries, Timeouts and Backoff in Go for the mechanics. For dependencies that can degrade for extended periods rather than fail cleanly, a circuit breaker sits in front of the retry loop so a struggling dependency gets a chance to recover instead of an ever-growing stream of retried calls — see Circuit Breakers in Go. Rate limiting sits at the API's own front door, protecting it from being overwhelmed regardless of how well-behaved its own outbound calls are — see API Rate Limiting in Go. And graceful shutdown is the reliability mechanism for the deployment lifecycle itself: every time a Go API pod is replaced, it needs to stop accepting new work, finish in-flight requests, and close its PostgreSQL and Redis connections cleanly before exiting — see Graceful Shutdown in Go HTTP Servers.

What happens if the broker is unavailable for ten minutes? Nothing is lost. The order transaction still commits (the outbox insert is in the same transaction as the order itself), events simply accumulate in the outbox_events table with a growing "oldest pending" age. The Worker Service and Notification Service fall behind, but catch up once the broker recovers — this is exactly the failure mode the outbox pattern is designed to survive gracefully, covered below.

Data

The data stack scales in a specific order, not all at once: correct transactions and locking first (a lost update or an oversold item is a correctness bug no amount of infrastructure fixes — see Database Transactions in Go), proper indexing next (see PostgreSQL Indexes), connection pooling as resource management rather than a scaling technique (see PostgreSQL Connection Pooling in Go), Redis as a cache-aside layer in front of hot reads (see Caching in Go), and read replicas only once reads — not writes — are the actual bottleneck and queries are already reasonably optimized (see Scaling PostgreSQL for Go Applications). Skipping ahead in this order — adding replicas before fixing an unindexed query, for instance — is a common and expensive mistake.

What happens if Redis goes down? In this architecture, reads fall back to querying PostgreSQL directly — higher latency and more load on the primary, but the system stays correct, provided the caching layer was built to fail open rather than fail the request outright when Redis is unreachable.

Messaging

The arrow from the API to PostgreSQL and the arrow that eventually reaches the broker are not the same write, and there's no transaction spanning both a database and a message broker — this is the dual-write problem, and the outbox in the diagram is how it's solved: the order and its event are written in one PostgreSQL transaction, and a separate worker relays pending outbox rows to the broker, retrying against a durable table instead of an unreliable network call. See The Outbox Pattern with Go and PostgreSQL for the full mechanism, including SELECT ... FOR UPDATE SKIP LOCKED for running multiple relay workers safely. Once an event reaches the Worker Service or Notification Service, both must assume they'll occasionally see the same event twice — that's the nature of at-least-once delivery, not a bug — see Event-Driven Architecture with Go for why, and Idempotent Consumers in Go for how each consumer protects itself with a processed-events table and a database unique constraint. A message that fails processing repeatedly should move to a dead-letter queue after a bounded number of attempts rather than blocking everything behind it or retrying forever.

Security

Authentication (who is this?) and authorization (what are they allowed to touch?) are separate checks, and the second one has to happen on every request that names a specific resource — not just once at login. Secrets (DATABASE_URL, the JWT signing key, broker credentials) come from environment variables or a secrets manager, never source code. Every query built with anything other than parameterized SQL is a SQL injection risk regardless of how trusted the input source seems. The full treatment, including a concrete checklist, is in Securing Go REST APIs.

Observability

Every box in the diagram needs to answer three questions after the fact: what happened (structured logs with a correlation ID that follows a request across the API, the worker, and the notification service), how much and how fast (request count, latency percentiles, error rate, exposed as Prometheus metrics), and where time went across service boundaries (traces). The API also needs two distinct health endpoints — a liveness check that only asks "is this process alive" and a readiness check that asks "can this pod actually serve traffic right now" (checking its PostgreSQL connection, for instance) — conflating them causes Kubernetes to kill healthy pods over a slow dependency. Full treatment in Observability for Go Microservices.

Deployment

The Go API and Worker Service each run as containerized processes behind a Kubernetes Deployment, with resource requests and limits sized to their actual usage, and a terminationGracePeriodSeconds longer than the graceful shutdown timeout each process needs. See Kubernetes for Go Developers for the Deployment, Service, and probe configuration this requires.

What happens during a rolling deployment? New pods only receive traffic once their readiness probe passes; old pods stop receiving new traffic the moment they're marked not-ready, but keep running until in-flight requests finish or the grace period expires. A Worker Service mid-processing when its pod is replaced needs the same discipline: either its unit of work is short enough to finish inside the grace period, or the work itself is resumable (which the outbox and idempotent-consumer patterns already make true, since a message that isn't acknowledged just gets redelivered).

Monolith vs. modular monolith vs. microservices

None of the above requires this system to be several separately deployed services. A monolith — the API, its business logic, and even the Worker Service running as one deployable — is the right default for most new systems: one thing to build, test, and deploy, no network calls where a function call would do, and no distributed-transaction problems because there's only one transaction boundary to reason about. It stays the right choice for longer than most teams assume.

A modular monolith applies the same clean-architecture layering consistently, with clear internal package boundaries between, say, orders, payments, and notifications — boundaries that could become service boundaries later, without paying the operational cost of running separate services before there's a real need to. This is the pragmatic middle ground: it keeps the option open rather than closing it off, without committing to it.

Microservices — actually splitting the API, Worker Service, and Notification Service into independently deployed services, as drawn in the diagram above — are justified by two things: multiple teams needing independent deploy cadences for different parts of the system, and genuine differences in scaling or reliability requirements between components (the Worker Service processing a burst of events has a very different load profile than the API serving synchronous requests, for instance). They are not justified by architectural fashion, and they add real, permanent costs: network calls where there were function calls, eventual consistency where there was one database transaction, and everything in the "reliability" and "messaging" sections above becoming mandatory rather than optional. Most systems described as needing microservices from day one didn't.

Production checklist

  • Graceful shutdown implemented (http.Server.Shutdown, resources closed in order)
  • terminationGracePeriodSeconds exceeds the application's shutdown timeout
  • Liveness and readiness endpoints exist and check different things
  • Secrets are not in source control — loaded from environment or a secrets manager
  • All database queries use parameterized SQL
  • Every write that must be atomic with a business change uses a real transaction
  • Indexes exist for the query patterns actually used in production, verified with EXPLAIN ANALYZE
  • Connection pool size is sized against the database's real connection ceiling, not chosen arbitrarily
  • Any cross-system write (database + message broker) uses the outbox pattern or equivalent
  • Every message consumer is idempotent against redelivery
  • Poisoned messages move to a dead-letter queue instead of blocking or retrying forever
  • Outbound calls to unreliable dependencies have a bounded timeout
  • Retries use backoff with jitter and a maximum attempt count
  • Circuit breakers exist on critical outbound dependencies
  • Rate limiting exists on the API's public entry points, especially auth endpoints
  • Object-level authorization is checked on every request naming a specific resource
  • Structured logs include a correlation ID that follows a request across services
  • Key metrics (latency percentiles, error rate, queue/outbox lag) are exported and alertable
  • Kubernetes resource requests and limits are set, not left as defaults
  • A rollout has been tested to confirm no dropped requests during a deploy

Summary

A production backend is not one hard problem — it's a set of ordinary problems (a slow dependency, a duplicate message, a pod being replaced) that each have a well-understood answer, arranged so the answers compose instead of overlapping or leaving gaps. Clean layering inside the API, reliability patterns on every outbound call, a data stack scaled in the right order, an outbox for anything crossing into messaging, security checked on every request rather than once at login, and observability and deployment practices that assume failure is normal rather than exceptional — that's the whole floor plan. Whether it runs as one deployable or several is a separate decision, driven by team structure and real scaling needs, not by how many of these boxes the diagram happens to have.