Tech Stack
Every choice here favours a boring, well-understood monolith over a distributed system. The goal was a platform one engineer can reason about end-to-end, that still scales to real traffic.
Core
| Layer | Choice | Why |
|---|---|---|
| Framework | Rails 8.1 | Mature, batteries-included, fast to build in |
| Language | Ruby | Expressive; pairs naturally with Rails |
| Database | PostgreSQL | Reliable, rich feature set, trigram search |
| Web server | Puma | Multi-threaded, the Rails default |
Front end
The UI is server-rendered and progressively enhanced — no separate SPA.
- ERB views rendered by Rails.
- Hotwire — Turbo for partial page updates and streams, Stimulus for sprinkles of JavaScript.
- ViewComponent — reusable, testable UI components under
app/components/. - Bootstrap 5 for styling.
Background work
- Solid Queue — database-backed jobs for transactional work (order confirmations, emails, OTPs).
- Sidekiq — Redis-backed queue for high-volume real-time broadcasts.
- 93 job classes in total, covering notifications, reconciliation, campaign delivery, and scheduled maintenance.
Auth & authorization
- Devise for authentication, with OmniAuth for social login, plus magic-link and OTP flows.
- Pundit policies for role-based authorization (customer, vendor, admin).
Infrastructure
- Redis — caching and rate-limit storage, deployed as a fail-open dependency.
- AWS SES (API v2) for transactional and campaign email.
- Docker for local development and production images.
- Kamal for zero-downtime deploys.
- Prometheus metrics and Sentry error tracking.
Quality
- RSpec with factory_bot for the test suite.
- Brakeman for security scanning, RuboCop for style.
- rswag generates an OpenAPI/Swagger spec for the public API.
- SimpleCov for coverage reporting.
Why a monolith
A microservice split would have added network hops, distributed transactions, and operational overhead — for a system that one team can run comfortably as a single deployable. The monolith keeps refactors cheap and the mental model small. Where isolation matters (background jobs, the public API), it is drawn with module boundaries, not service boundaries.