Blog

Product news & updates

Building Monolithic Web Services

The microservices vs. monolith debate never really dies. Every few years it gets rebranded, and most teams keep picking microservices because microservices feel modern and architecturally serious in a way monoliths don't.

At sparQ, we picked microservices too. Then we undid it.

DHH was right (and it hurt to admit)

David Heinemeier Hansson, the creator of Rails and co-founder of Basecamp, has been pushing the "majestic monolith" argument for years. His point is simple. Microservices add operational complexity that most teams can't justify. You're paying for distributed systems overhead before you've earned the scale that would make it worth it. For most teams most of the time, a well-structured monolith is faster to build, easier to reason about, simpler to debug.

DHH is controversial because he says this loudly and doesn't soften it for the people who already went the other way. After going through the microservices lifecycle ourselves at sparQ, the argument landed harder than I wanted it to.

What we actually tried

Our first architecture split services the textbook way. Each concern got its own service. Each service owned its own deployment. On paper, clean. In practice, every change touched multiple repos, every bug trace crossed service boundaries, and our small team burned real engineering hours on infrastructure coordination that added zero product value.

The seams between services were supposed to be the feature. They became the friction.

So we consolidated. Not because separation of concerns stopped mattering (it didn't), but because we wanted the operational benefits of a monolith without losing the structural clarity we'd gotten used to. One deployable unit. Internal services still meaningfully isolated. A consistent API surface in front of all of it.

Why Go, and why it matters here

The core sparQ application is Python, and we like Python for application logic. Expressive, fast to iterate on, ecosystem is deep. For the services layer, though, we picked Go.

Go compiles to a single binary. No runtime to manage, no interpreter to ship, no dependency chain to untangle at deploy time. You build it, you ship it, it runs. When something breaks, you're not fighting the language.

The concurrency model also fits the workload. Our services do I/O-bound work: sending emails, dispatching SMS, parsing structured data. Go handles that kind of parallelization without the ceremony threading requires in other languages. The raw performance is there when you need it.

What the service layer actually looks like

The consolidation gave us a clean pattern. Each service lives at /api/v<x>/<service> and talks to the Python application as its primary client. The application is the source of truth for business logic. The services handle execution.

Take SMTP. Our SMTP service handles notification emails, the transactional stuff sparQ sends when something happens a user needs to know about. It has one client: the application. It doesn't need to know why an email is being sent, or what business rule triggered it. It takes a well-formed request, delivers it reliably, and stays out of the way. The interface is narrow on purpose.

SMS and our parser service work the same way. Each does a specific job, exposes a versioned endpoint, stays decoupled from its neighbors. Monolith here doesn't mean everything is tangled together. It means everything lives in the same deployable unit with a shared operational footprint.

What we actually got

The consolidation gave us back something microservices had quietly taken: the ability to trace what's happening. When a notification doesn't go out, we're not crossing service logs in three different places trying to reconstruct a timeline. The failure surface is smaller. The path from "something went wrong" to "here's exactly what went wrong" is shorter.
We also got deployment simplicity. One build, one artifact, one deploy. For a small engineering team that's not a minor convenience, it's hours back every week.

The majestic monolith isn't a step backward, and it isn't a ceiling either. A well-structured monolith scales. What it doesn't do is make you pay for distributed systems complexity before you actually need it. The pattern we landed on (Go services, versioned endpoints, one deployable unit with clean internal boundaries) is something we're confident in for the long haul. DHH has been saying this for years. It took us a microservices detour to figure out why he's right.