Why We Chose Go for Our Backend Services

| 5 min read |
golang go backend engineering

How Go became the default backend language at Dropbyke and a fintech startup, what it replaced, and the honest tradeoffs we accepted along the way.

At Dropbyke we had a fleet of shared bikes, a mobile app, and a backend stitched together from Python and Node services. The system worked. Mostly. But every time we pushed past a few hundred concurrent riders the cracks showed: event loops choking on CPU-bound geofencing, Python processes eating RAM, and deploys that required matching the exact virtualenv on the target box or everything fell over.

The fintech startup had a different shape but similar pain. Financial news aggregation means constant HTTP polling, WebSocket fanout, and ML pipeline orchestration. The Node services were fast to write and slow to operate. Debugging a callback-hell crash at 3 AM on a service that processes market data isn’t my idea of a good time.

We needed something that compiled to a single binary, handled thousands of concurrent connections without drama, and let a small team move fast without an ops nightmare.

Quick take

Go won us over with fast compiles, dead-simple deployment, and goroutines that actually scale. The error handling verbosity is real and generics are nowhere in sight, but for network-heavy backend services in 2016, nothing else hit the same balance of speed, simplicity, and operational sanity.

Why Go

I started experimenting with Go around 1.5 and by the time 1.6 and 1.7 landed, I was convinced. Three things sold me.

Compilation speed. Our Python and Node services had no compile step, which sounds like an advantage until you realize the “compile step” just moved to production as runtime errors. Go gave us a real compiler that caught problems early and still built a full service in seconds. Coming from a brief stint with Java microservices, this felt like cheating.

Single binary deploys. One binary. No runtime. No dependency manager on the server. No “works on my machine.” We could cross-compile for Linux on a Mac, scp the binary to a server, and it ran. Later we moved to Docker, but even there the images were tiny because there was nothing to install.

Goroutines and channels. This was the real hook. At Dropbyke, every bike sends a GPS heartbeat every few seconds. At the fintech startup, every news source gets polled on its own schedule. Both problems are embarrassingly concurrent. Go made that concurrency feel natural rather than bolted on.

Here is a simplified version of how we handled bike heartbeats:

func processHeartbeats(heartbeats <-chan Heartbeat, store *BikeStore) {
	for hb := range heartbeats {
		go func(hb Heartbeat) {
			if err := store.UpdatePosition(hb.BikeID, hb.Lat, hb.Lng); err != nil {
				log.Printf("failed to update bike %s: %v", hb.BikeID, err)
				return
			}
			if outsideGeofence(hb.Lat, hb.Lng) {
				alertOps(hb.BikeID)
			}
		}(hb)
	}
}

Nothing clever. No framework. Just goroutines, a channel, and straightforward control flow. A junior engineer could read this and understand what happens. That mattered to me more than elegance.

What we gained

The improvements were immediate and measurable.

Memory usage on the bike tracking service dropped from around 400 MB under the Python version to about 30 MB with Go. Tail latency at p99 went from unpredictable spikes to a consistent sub-10ms for the heartbeat path. Deploy time went from “run Ansible and hope” to “copy binary and restart.”

At the fintech startup the news ingestion pipeline handled 3x the source count on the same hardware after the rewrite. We stopped worrying about whether a stalled HTTP client would block the event loop because there was no event loop to block.

The standard library deserves credit too. net/http, encoding/json, crypto/tls – all solid, all built in. We pulled in very few external dependencies in the first year, which meant fewer things to audit and fewer things to break.

The honest downsides

Go isn’t perfect. I would be lying if I said the tradeoffs didn’t sting sometimes.

Error handling verbosity. if err != nil after every single call. I’ve written that line thousands of times. It makes control flow explicit, which I genuinely value, but it also adds noise. Some files are 40% error checks. You get used to it, but it never stops being verbose.

No generics. In late 2016 this is Go’s most obvious gap. Every time I write a utility function that works on a slice, I either duplicate it for each type or reach for interface{} and lose type safety. We ended up with a pkg/sliceutil package full of type-specific helpers that felt like something a code generator should handle. It works, but it isn’t satisfying.

Smaller ecosystem. Python has a library for everything. Go in 2016 has a library for most things, but the quality varies and some areas are just thin. ORM support, for instance, was rough. We ended up writing raw SQL with database/sql and honestly that turned out to be a better long-term choice, but it was more work upfront.

Dependency management. In 2016, Go’s dependency story is a mess. Vendoring, godep, glide, and no official solution. We settled on glide and it mostly worked, but it wasn’t a confidence-inspiring experience. This is the one area where I envied the Node and Python ecosystems.

What I would tell another CTO

If your backend is mostly network services – APIs, data pipelines, real-time processing – Go is a strong default in 2016. The language is deliberately boring, and boring is a feature when you’re running production systems with a small team.

Don’t adopt Go because it’s trendy. Adopt it because you want fast compiles, predictable performance, and deploys that don’t require a prayer. Then accept the verbosity and the missing generics as the price of admission.

Start with one service. Pick the one that hurts the most operationally. Rewrite it in Go, measure the difference, and let the results speak. That’s what we did at Dropbyke with the heartbeat service, and within two months every new backend service was Go by default.

Looking ahead

Go 1.7 just landed with context in the standard library, which cleaned up a lot of our cancellation and timeout patterns. The tooling keeps getting better. gofmt alone has saved us more arguments than any code review policy.

I don’t know if Go will get generics. I hope it does. But even without them, the language has earned its place in our stack through discipline and simplicity. Not everything needs to be expressive. Sometimes you just need it to work, to be fast, and to be obvious.

That’s Go for me. And I’m not going back.