Where we pick boring tech, and where we don't
A client asked us last month why their new dashboard runs on Postgres and a Rails API when their previous vendor had pitched a serverless GraphQL layer with three managed services. The honest answer is that we can maintain Postgres and Rails at 2 AM, and we can hand it to the next developer without a week of orientation.
We default to boring technology because we stay on after launch. Most of our revenue comes from maintenance contracts, not greenfield builds. A stack that looks impressive in a proposal but costs us four hours of debugging on a Saturday is a stack that loses money. So we reach for tools with long documentation tails, known failure modes, and answers on Stack Overflow that are still correct.
Our defaults: Postgres for storage, Rails or Express for APIs, React for frontends, Sidekiq for background jobs, Redis for caching and queues, Docker on a single EC2 instance or ECS depending on load. We use managed Postgres (RDS) because we have been woken up by replica lag and do not want to manage replication ourselves. We use Sentry for error tracking because it catches the things our tests don't, and every project ships with it from day one.
This is not a philosophy. It is a maintenance budget. Three people cannot keep up with a stack that changes quarterly.
There are three places where we deliberately go the other way.
**Full-text search.** We use Postgres `tsvector` for simple cases — a dozen fields, under 100k rows, no faceting. Beyond that we reach for Meilisearch. We made this call after a client's product search hit 800ms on Postgres with a trigram index across three joined tables. Meilisearch brought it under 50ms and the relevance improved. The tradeoff: another process to run, another data sync to monitor, and a small risk of drift between Postgres and the search index. We accept that because search latency is a user-facing metric that shows up in conversion numbers.
**PDF generation.** We tried wkhtmltopdf, Puppeteer, and headless Chromium. They all work until you need precise layout — page breaks at the right spot, repeating headers, table rows that don't split across pages. For invoices and reports that go to clients, we use a commercial library called DocRaptor, which wraps PrinceXML. It costs money per document. The alternative is spending an afternoon fighting CSS print rules every time a client changes their invoice template. We bill for the DocRapter cost directly and clients prefer it once they see the output.
**Scheduled jobs with complex dependencies.** Sidekiq handles 90% of our background work. But when a job needs to wait on three others and then trigger two more, we use Temporal. We adopted it for a client whose ERP sync pulls orders, checks inventory, generates invoices, and notifies a courier — in that order, with retries and human-approval steps. Temporal makes the dependency graph explicit and recoverable. The tradeoff is operational weight: it needs its own database and a worker fleet, and the mental model takes a few days to click. We use it only when a workflow has more than five steps with branching paths. Below that, Sidekiq batches are enough.
Everything else, we keep boring. We do not use a service mesh. We do not run Kubernetes for a three-service deployment. We do not add a message queue until we have felt the pain of not having one. We have never regretted choosing the simpler option. We have regretted the opposite — a client came to us after a previous team built their checkout on a microservice architecture with four repos and an event bus. It worked, but nobody on the original team could explain the full request path. We consolidated it into a single Rails app over six weeks and the deploy time dropped from 25 minutes to 3.
The principle is simple: every piece of technology we add is something we will maintain for years. We choose accordingly.