Newsletter
Join the Community
Subscribe to our newsletter for the latest news and updates
2025/08/16
Start with a modular monolith to ship fast—and keep options open to scale.
Make “scale” a product habit: observability, zero-downtime changes, and performance baselines from week one.
Design your data model for multi-tenancy and future sharding without over-engineering.
Split services only when boundaries are clear and the pain of staying monolithic is measurable.
Borrow battle-tested lessons from Shopify’s modular monolith and pragmatic microservices advice.
Most SaaS teams associate “scaling” with traffic spikes and database size. In reality, the first scalability ceiling you’ll hit is developer velocity: shipping changes safely and quickly as your product and team grow. The best hedge is to design for scale on day one—not by scattering microservices, but by setting foundations that keep you fast, reliable, and adaptable. Below is a practical playbook, backed by lessons from Shopify’s modular monolith and cautionary experience from industry veterans.
Default to a modular monolith. Keep the app in one codebase and process, but enforce clear internal boundaries. This maximizes delivery speed and minimizes operational overhead early on. Shopify has run one of the world’s largest Rails monoliths and invested heavily in modularizing it to preserve velocity as complexity grew (see Shopify’s engineering write-up on their modular monolith) Under Deconstruction: The State of Shopify’s Monolith.
Avoid premature microservices. They’re a coordination and ops tax most startups can’t afford. As David Heinemeier Hansson argues, start with a “majestic monolith” and only extract when it solves a proven bottleneck or creates clear organizational benefits How to recover from microservices.
Define components/modules around domains (e.g., Billing, Auth, Reporting). Each component owns its data access and exposes well-defined interfaces.
Enforce boundaries with lightweight rules: explicit dependencies, linter checks, and code review guidelines. Shopify reports that componentization improved onboarding, sped up tests by narrowing scope, and clarified ownership across teams.
Keep cross-component contracts stable. This lets teams refactor internals without breaking others.
Prefer composition over shared globals. Inject dependencies rather than reaching across modules.
Write contract tests (or smoke tests) for component interfaces.
Keep background jobs and HTTP handlers thin; push domain logic into testable services inside modules.
Scope every row to a tenant_id. Index it. Avoid cross-tenant joins. This makes future sharding (by tenant) far simpler.
Use stable, globally unique identifiers (UUIDs) for external references and messaging.
Isolate large, write-heavy tables early (e.g., events, logs) into their own schemas or databases so they don’t throttle core OLTP workload.
Adopt “expand/contract” as a habit: add new columns/tables first, backfill, dual-write if needed, then cut over and remove old fields.
Never ship code that depends on a migration still in progress. Deploy order: expand schema → deploy app changes → contract schema.
Keep migrations small and reversible. Practice rollbacks.
Caching hierarchy: client/browser caching, CDN for static assets, application-level caching for read-heavy views, and database indices designed from real query plans.
Background processing: make jobs idempotent, retry-safe, and bounded. Track queue depth and latency as first-class metrics.
Circuit breakers and timeouts: don’t let one dependency stall your whole app.
Rate limiting and backpressure: protect critical paths (e.g., sign-up, checkout, billing webhooks).
Performance budgets: set target latency for key flows; alert when regressions cross thresholds.
Tip: Invest in fast tests that map to components. Shopify notes that running tests only for affected components sped up feedback loops and improved stability.
Observability: instrument logs, metrics, and traces. Give every request a correlation ID. Track SLOs (e.g., 99.9% success and latency targets) for the top three user flows.
Progressive delivery: feature flags, canary releases, and dark launches. Build a culture of shipping small, observable changes.
Safe deploys and rollbacks: automations to migrate, deploy, verify, and roll back quickly. Practice failure injection on non-critical paths.
Data safety: automated backups, restore drills, and encryption in transit and at rest.
Only consider extraction when:
Pragmatic extraction sequence, echoing DHH’s guidance:
For very large organizations with many teams, domain-oriented service ownership can work—but it’s a graduation path, not a starting position. See Uber’s perspective on domain-oriented microservices for context.
Shopify’s modular monolith: Shopify runs a massive Rails monolith (millions of LOC) and invested in a componentized architecture to keep developer productivity high. Benefits included clearer ownership, targeted exception triage, and the ability to run on the newest Rails revisions due to well-defined boundaries and responsibilities. For background on the approach, see their earlier post on deconstructing the monolith into a modular monolith.
Caution on microservices: DHH outlines a practical recovery plan for teams that split too early—consolidate critical flows, standardize tech choices, and learn to create boundaries with modules before networks.
Architecture
Data
Performance
Operations
Team practices
The most reliable way to “scale from day one” is to make scale an everyday habit: clear internal boundaries, safe change patterns, and operational hygiene. Start with a modular monolith to move fast without burying yourself in distributed-system complexity. Observe your system, measure your pain, and only split when the benefits are obvious and the boundaries are ready. That’s how you protect both delivery velocity and long-term resilience.