Monolith for life, except when not — how we split CloudPrint out of the ORDR Rails app
ORDR is a Rails monolith and will stay one. CloudPrint, the printer microservice, had to live somewhere else — but for the boring physical reason that it talks to a thermal printer in a venue, not because the monolith was failing us. The rule we use: split out only when the constraint forces it.
By Carlos Butler
Co-founder, engineering
For about a decade now, microservices have been the default architectural reflex of the trade press. “Break the monolith” is on every conference agenda and every consultant’s slide deck. There is real value in the genre — but for a team our size, the value is mostly negative. The cost of a service boundary is enormous, and most of the time the constraint that would force one to exist simply is not present.
ORDR is, today, a Rails monolith. One repository, one deploy, one Postgres database. We have one production microservice — CloudPrint — and it is one because it has to be, not because we wanted it.
This post is the long version of why we stay monolithic and how we decided when not to.
The case for the monolith, briefly
David Heinemeier Hansson — the creator of Rails — has been writing about this consistently for fifteen years now. His hey.com blog is the single best source on it, and the older Signal v. Noise piece “The Majestic Monolith” is the canonical articulation.
The argument, distilled: a single coherent codebase with a single deployment unit is the lowest-friction substrate for a small team to ship product. You get atomic refactors. You get cross-cutting changes in a single PR. You get one log to read when something is wrong. You can change a database column and the code that references it in the same commit.
Martin Fowler’s “MonolithFirst” piece makes the corollary point: even teams that end up at microservices usually wish they had started monolithic. Splitting an existing monolith into services after you understand the domain is a much easier engineering job than predicting service boundaries before you do. Lewis and Fowler’s later “Microservices” essay is honest about the same trade-offs — distributed systems impose real costs that you do not pay in a monolith.
For ORDR specifically, the math is unforgiving. We are one engineer. Adding a service multiplies our operational surface — another deploy pipeline, another runtime, another log stream, another network failure mode, another set of monitoring, another version-skew problem on every release. Every one of those costs is paid every day. The benefits — independent scaling, isolated failures, polyglot stacks — are benefits we genuinely do not need until we have ten engineers and a hot path running ten thousand requests a second. We have neither.
Shopify, who run a much larger team than us on what they call a “modular monolith”, have written about the same trade-offs at scale. Their conclusion: bound the domain inside the monolith, isolate modules from each other in code, but resist the urge to split into a network of services until you have a forcing function.
Our rule
So we use a simple rule: stay monolithic until the constraint forces a split. Not until “the codebase feels big”. Not until “we want to scale independently”. Not until “another team takes over part of the code”. A real physical, regulatory or external-system constraint that the monolith cannot satisfy.
Today, we have one such constraint, and it lives in the kitchen.
Why CloudPrint had to leave
ORDR talks to thermal receipt printers — the small Star Micronics units that sit in the kitchen and at the till. Those printers do not speak HTTP in any normal sense. They poll an endpoint every few seconds, retrieve a binary blob in Star’s own document-markup format, and report the print job’s status back. The protocol is called CloudPRNT and Star documents it openly for any developer to integrate.
Two things about this protocol are awkward for our monolith.
It is high-frequency polling. Hundreds of printers across hundreds of venues, polling every few seconds, all day. If those polls hit the main Rails app and a shared Postgres database, we are doing thousands of unnecessary writes per second just to record “this printer is still alive”. Postgres can do that. It is a waste of Postgres.
It is unauthenticated. The printer cannot present credentials in any meaningful sense. It identifies itself by MAC address and trusts the server. That is fine — but it means the endpoint must be hardened differently from the rest of the app, with separate rate-limiting, separate firewalling, and an explicit boundary so a compromised printer cannot reach into the Rails session store.
The first problem is solvable in the monolith with Redis-backed presence tracking — and we did exactly that, for a year, before we split. The second is solvable too, with careful routing rules. But what is not solvable in the monolith is the third thing, which is deploy independence at the boundary.
Every deploy of the Rails app risks a thirty-second blip while Puma restarts. For a customer-facing flow, thirty seconds is recoverable. For a printer that is mid-job at the till during a busy service, thirty seconds is a missed receipt and a confused customer. Splitting CloudPrint into its own service means we can deploy the Rails app freely without worrying about a printer somewhere being mid-poll.
That third constraint — the printer’s intolerance of even a brief restart — is what made the split unavoidable.
So CloudPrint is its own small Ruby on Rails (API-only) service, sharing the same Postgres cluster as the monolith, but with its own ECS service, its own deploy pipeline and its own restart cadence. It does one thing — manage the printer protocol — and it does it well. The Rails monolith sends print jobs to CloudPrint over an internal API, and CloudPrint takes care of the rest.
The split was painful enough — a different deploy pipeline, a separate set of secrets, a network boundary to monitor — that we have not done it for anything else. We have considered splitting out our event-ticketing flow. We have considered splitting out the customer-facing ordering app. In every case, the answer has been: the monolith handles it fine. We will revisit when it does not.
What we gave up
A clean justification for resume CVs. “Microservices architect” reads better than “Rails monolith maintainer”. I am at peace with this. The codebase ships faster than it would otherwise.
The ability to scale CloudPrint independently in a meaningful way. In theory we could now run twenty CloudPrint replicas during peak; in practice we run two, and the bottleneck is Postgres and the printers themselves, not CloudPrint’s runtime. The split bought us deploy isolation, not scale.
The ability to choose a different language for CloudPrint. We considered Go, briefly, for the polling endpoint. We stuck with Ruby and Rails because the cost of polyglot in a one-engineer team is enormous, and Ruby’s request throughput at this workload is fine. The TechEmpower benchmarks put Rails comfortably above what a printer-polling workload needs.
What ORDR does about this
ORDR’s product surface — staff app, customer app, payments, reporting, menus, events — runs on a single Rails 8 monolith with one Postgres database. We split out CloudPrint when the printer protocol forced our hand, and we have stayed honest about not splitting anything else until something else forces us. If you are a small team considering a similar architectural choice, the rule is uncomplicated: monolith for life, except when not.