Choosing an architecture — a decision framework
No stack is universally best. Match the workload, the team, and the deployment target. Start boring, scale up complexity only when measurements force you.
Workload-driven decisions: (a) CRUD-heavy transactional app → Django / Rails / Laravel / Phoenix — batteries win. (b) Real-time / high-concurrency (chat, presence, collaborative editing) → Go, Elixir/Phoenix, Node with Socket.IO/ws. Concurrency primitives matter. (c) Data-heavy + ML pipeline → Python (FastAPI + pandas / torch / sklearn). Ecosystem is unmatched. (d) Ultra-low latency + CPU perf (trading, ad serving, game servers) → Go, Rust, sometimes JVM. (e) UI-first product with heavy type safety → Next.js / Remix with RSC. Start by characterizing the workload, then pick the stack — not the reverse.
Team-driven decisions: velocity = framework × team-skill. A strong Python team on a greenfield Rails app is strictly worse than the same team on Django, even if Rails is 'better' for the workload. Pick the stack your team can ship confidently. Re-training cost is real; 'better' architectures that take 6 months to learn are worse than 'adequate' ones that ship in 2 weeks. Exception: strategic bets on new tech for 6-month+ projects with senior teams who want the investment.
The monolith-first argument (Martin Fowler): start with a modular monolith. Extract services ONLY when you have a real boundary — different scaling profile (image resize vs API), different team (separate squads), different release cadence (billing code freeze), or different technology need (ML service in Python, main app in Go). Premature microservices = distributed monolith = all the pain (network latency, partial failures, deploy coordination), none of the gain.
Framework lifecycle risk: how old is the framework, how fast does it break? Django / Rails / Spring: 20+ years of stability, 5-year LTS. Next.js: ships breaking changes yearly (App Router, Server Actions, React 19 integration). Choose based on your tolerance: stability for long-term products + minimal maintenance; cutting-edge for rapid iteration + new-feature velocity. Running a Next.js app long-term means budgeting 2–4 weeks per year on framework upgrades.
Community + hiring: Python and JS devs are abundant. Elixir, Rust, OCaml, Scala devs are rarer and more expensive. If you pick a stack no one locally knows, you've constrained hiring, code review, and long-term support. For small teams, this can outweigh any technical metric. For large orgs, a specialized team with a fit-for-purpose stack (e.g., Scala for high-throughput data) can pay off — but only if leadership budgets the hiring effort.
Decision matrix exercise for any non-trivial project: rank candidate stacks (3–5 of them) across: concurrency fit, ecosystem fit, team fit, deployment fit, hiring pool, stability/upgrade cost, operational complexity. Weight the rows by what matters for YOUR product. No winner in all rows? Expected — you're trading off. Pick the stack that wins the most-weighted rows; document the trade-offs you accepted.
Specific anti-patterns to avoid: (i) choosing by TechEmpower benchmark (your bottleneck is the DB, not framework overhead). (ii) choosing by HN hype (2-year-old HN darlings are often abandoned). (iii) copying FAANG architecture (Google-scale problems; startups have different problems). (iv) rewriting in the hot new thing mid-project (existing code has shipped tests, muscle memory, known bugs — the rewrite loses all of that). (v) over-engineering for scale you don't have (build for 10× current, not 1000×; you'll refactor when you need to).
Grounded on https://martinfowler.com/bliki/MonolithFirst.html