Published on
Creator & maintainer ·

Go-SimpleScale: a monolith that becomes microservices without a rewrite

A Go monorepo where the same business logic runs as a single binary or as three independent services — you switch by how you run it, not by changing code.

Go 1.24chiPostgreSQLKafkaJWT + API keysPrometheusDockerKubernetes
Go-SimpleScale: a monolith that becomes microservices without a rewrite

Start as a monolith, scale to microservices when you actually need to. The same business logic runs as a single binary or as three independent services — you switch by how you run it, not by changing code. Security is wired into every deployment mode: authenticated routes, no compiled-in secrets, atomic inventory, and per-order ownership checks.

The dilemma it kills

Every new backend faces the same fork, and both paths hurt:

  • Start with microservices and you pay for distributed systems on day one — network calls, eventual consistency, five things to deploy — before you have the traffic or the team to justify any of it.
  • Start with a monolith and you move fast, right up until the day you need to split it and discover the boundaries were never real.

Go-SimpleScale takes a third path: a modular monolith with boundaries that are real from day one, packaged so the exact same code deploys as one process or as separate services. You don't choose up front and live with the consequences — you defer the choice until you have the information to make it.

The core idea

Three example services — users, products, orders — each follow clean architecture (domain → application → adapters) and each is its own Go module. The trick that makes both deployment modes work from one codebase is a small one: a service depends on the interfaces it needs, not on other services.

The order service, for instance, defines its own UserService and ProductService interfaces describing only what it needs (does this user exist? what does this product cost? reserve this stock). At startup, the entry point hands it either the in-process implementations (monolith mode — a direct method call) or HTTP clients (microservices mode — a network call). The order service's code never knows the difference.

Monolith:       order ──(Go interface)──▶ in-process user/product services
Microservices:  order ──(Go interface)──▶ HTTP clients ──▶ user/product APIs

The result: the monolith and the standalone services expose the same routes with the same authentication, so clients don't change when you split. Moving a service out is an operational decision, not a rewrite.

Built to be worry-free on the fundamentals

The thing that separates a starter you can actually build on from a toy is whether the boring, critical stuff is done right. This one treats "secure and correct by default" as the whole point:

  • Authentication in every mode. Every API route requires a valid API key or JWT — in the monolith and in each standalone service. Secrets (JWT signing key, service API keys) are required at startup with no fallback: the binaries refuse to boot without them, so there's no way to accidentally ship the well-known dev secret that these templates usually leak.
  • Authorization, not just authentication. Orders belong to the authenticated caller; you can't read or cancel someone else's order, and prices come from the product catalog rather than the request body (no client-set prices).
  • Correct concurrency. Stock is decremented with a single atomic UPDATE … WHERE stock >= qty, so two people buying the last unit can't both win — the classic oversell race, closed.
  • Real middleware, not stubs. Rate limiting, request logging, panic recovery, CORS, timeouts, and body-size limits are implemented and tested, not placeholder functions that quietly do nothing.

Engineering decisions worth calling out

  • Swappable event bus. Services communicate through events over an interface that's backed by an in-memory bus in the monolith and Kafka in microservices mode — the same publish/subscribe code either way.
  • Versioned, per-service migrations. Each service owns its schema with its own migration history; the monolith applies all of them, each service applies only its own — one source of truth, no ad-hoc table creation.
  • Honest documentation. The docs describe only what's actually implemented; aspirational features live in a clearly-labeled roadmap. The README even lists the real limitations (float money, best-effort stock compensation, at-least-once event semantics) rather than pretending they don't exist.
  • Tests where the risk is. Coverage concentrates on the parts that bite — auth, the event bus (race-detector clean), order pricing/stock/ownership — rather than chasing a coverage percentage.

What it demonstrates

Pragmatic architecture judgment: knowing that most teams don't need microservices yet, but building the seams so they're a config change away when they do. And a bias toward getting the unglamorous fundamentals — auth, secrets handling, concurrency, honest docs — right before adding features.