3
4 Comments

Rails vs Go: what I learned building the same SaaS starter twice

I built two SaaS starter templates, one in Rails, one in Go, same feature set, same goal. The experience of building them was different enough that I wanted to write up the actual tradeoffs, not a hot take on which is "better."

Where Rails' opinions paid off

Auth. Devise gave me registration, login, sessions, password reset, and account confirmation basically for free. Go has no equivalent default. I ended up rolling my own JWT exchange: issuing tokens, handling expiry, thinking through revocation. None of it was hard exactly, just a lot of surface area I had to design and test myself instead of configuring.

Admin. ActiveAdmin gets you a working, auth-gated CRUD panel in minutes. Go has nothing comparable baked into the ecosystem. Want an admin panel? You're building one or bolting on a separate tool.

Same pattern both times: Rails had already made an opinionated, battle-tested call, and I just opted in.

The bigger thing underneath both of those: decision fatigue

Rails is an MVC framework with a narrow purpose, and the community has spent two decades converging on "the" way to do things. Go doesn't have that. It's used across CLIs, infra, backend services, all sorts of shapes, so a lot of questions a Rails dev never even has to think about are wide open in Go.

Clearest example: how do you talk to the database?

  • Rails: Active Record. That's the answer. Migrations, validations, associations, all designed to work together.
  • Go: open question. An ORM like GORM or ent that's trying to approximate Active Record but isn't nearly as battle-tested in this pattern? Or sqlc, a completely different philosophy, write raw SQL and generate typed Go from it?

That's not "Go is missing an ORM." It's "you now have to survey an ecosystem, pick a paradigm, and live with it," which is a real cost on its own, separate from any single feature gap.

Where Rails' opinion was overkill

Here's where it flips. My registration flow needed to send a confirmation email. In Rails, that meant I needed a queueing system, full stop, Sidekiq + Redis or Solid Queue + Postgres, before I'd shipped anything. In Go, I spun up an additional goroutine. No new dependency, nothing to configure. For a simple fire-and-forget case, that was exactly the right amount of solution.

To be fair to Rails: a goroutine isn't a queue. No crash durability, no retries with backoff, no dashboard for stuck or failed jobs. Once your job needs get more serious than "send one email and move on," you'll want what Solid Queue gives you out of the box. But for the simple case, Rails made me pay an infra decision I didn't need yet, and Go let me skip it until I did.

Go's wins that have nothing to do with what it's missing

It'd be easy to read the above as "Rails wins, Go just has less." That's not the whole story:

  • Resource footprint. My Go template runs comfortably on a fraction of the RAM the Rails one needs. That's the actual reason I sized my hosting the way I did, a real, measurable cost difference for a solo dev.
  • Compile-time safety. Type errors and nil checks caught at build time instead of surfacing at runtime, sometimes in production.
  • Deployment simplicity. A single static binary, no gem/bundler/version-manager juggling.
  • Concurrency as a first-class primitive. Goroutines are cheap and built into the language, which is exactly what made the "just add a goroutine" queueing story possible in the first place.
  • Explicitness. No callback chains or metaprogramming to trace through. What you see is what executes.

Takeaway

No universal winner. If you want a decision made for you so you can move fast, Rails' opinions are worth a lot. If you want a lighter footprint and don't mind making (and living with) your own calls, Go rewards that.

I ended up building starter templates in both stacks, mostly because I didn't want to make either set of tradeoffs more than once. You can check out live demos at rdooley.dev. Curious how others here spin up new ideas to prototype/test. Does the resource footprint and simplicity of something like Go win out when you're just trying to validate fast, or do you still reach for the batteries-included, built-for-the-purpose tool even at the prototype stage?

on August 13, 2026
  1. 1

    The decision fatigue point resonates a lot. I'm running FastAPI for my
    backend, and the database layer is exactly where I felt this, no single
    obvious answer like Active Record, just a fork between SQLAlchemy
    (closer to the "batteries included" feel) and something more raw like
    this piece describes for Go.

    The goroutine vs queue tradeoff is a good example of paying infra cost
    before you need it. I hit something similar early on, spun up a full
    task queue for something that honestly could've been a background task
    for months before it needed real durability.

    To your question, for prototyping I lean toward whatever has the least
    setup friction even if it means re-deciding things later. Validating
    fast usually matters more early on than getting the "correct" long-term
    architecture on day one.

    1. 1

      Thanks for the insight! Im glad that unnecessary infra/architecture decision early on resonated with someone else.

      If you don't mind me asking, when you decide that you are going to prototype a new idea, do you find yourself reaching for the same tools, or are you spending time figuring out what fits best before implementation?

      1. 2

        Honestly, I default to the same stack now, Next.js + FastAPI, mostly
        because I've built over 100 tools on it at this point and the friction
        of switching would outweigh whatever marginal fit a different stack
        might have for a specific idea.

        That wasn't always the plan though. Early on I did spend time
        evaluating options, but once the stack proved it could handle a wide
        range of tool types (PDF processing, image manipulation, calculators,
        converters) without hitting a wall, "figuring out what fits best"
        stopped being worth the time cost. Familiarity compounds, every new
        tool ships faster because I already know where the rough edges are.

        I think I'd only break that default if I hit something structurally
        different, like needing real-time/websocket-heavy behavior, or
        something with genuinely different resource constraints. Otherwise
        the "right tool for the job" calculus mostly loses to "the tool I can
        ship fastest and debug at 2am."

  2. 1

    The decision-fatigue point is probably the most interesting part here. The tradeoff isn't really Rails vs Go so much as how much of the architecture you want the framework to decide for you.