Blogs

Architectural decisions are trade-offs, not answers

There is rarely a correct architecture. There is a set of trade-offs, a set of constraints, and a decision that fits them or doesn't. That framing sounds obvious, but most bad architecture comes from ignoring it: picking microservices because they're modern, rewriting because the old code is ugly, or building in-house because control feels safer. Below are seven takeaways for making these calls deliberately, drawn from the common decision points every team eventually hits.

1. Understand the problem before touching the whiteboard

When someone says "we need microservices," keep asking why until you reach the actual need: scaling, team autonomy, deploy frequency, or sometimes nothing at all. Then write the problem and its constraints (performance targets, budget, team skills, compliance rules) on a single page. If you can't fit it on one page, you don't understand it yet, and fuzziness on paper becomes a mess in production. Talk to product, business, and ops before deciding anything; they know constraints you don't. And design for the core use cases first. A system designed around edge cases does nothing well.

2. Couple synchronously only when you need the answer

For a payment service that must trigger order fulfillment and inventory updates, chained REST calls work on a quiet Tuesday and collapse in a flash sale: one slow downstream service delays everything, one dead service fails the payment, and you inherit retries and distributed rollbacks. Publishing an event instead (payment confirmed, consumers react independently) buys loose coupling, independent scaling, and the ability to add a new consumer without touching existing code. The honest cost is operational: a broker to run, duplicate messages to handle, eventual consistency to explain. The useful heuristic: use synchronous calls when the caller genuinely needs the response to proceed, and events when it only needs the work to happen.

3. Buy the implementation, but own the abstraction

For anything security-critical and commodity, like authentication with MFA, SSO, and compliance requirements, buying (Auth0, Cognito) beats building. Vendors have absorbed years of attacks and audits your team hasn't. But there's a refinement worth adopting, raised by a reader of the original piece: build the abstraction, buy the implementation. Put your own interface between your code and the vendor, so your application depends on your AuthProvider, not on Auth0's SDK scattered through every module. The day stakeholders announce they want the product on premise (it happens), you swap one implementation behind an interface instead of performing surgery on the whole codebase. Buying saves you from building; the abstraction saves you from the vendor.

4. Start with a modular monolith

Before product-market fit, speed of iteration is worth more than theoretical scalability. A monolith deploys in one piece, debugs in one process, and refactors without network calls. The trick is the modifier: modular. Keep clean internal boundaries between billing, user management, and notifications, so that when a real scaling need appears you can extract that module into a service. Going microservices on day one means paying for distributed transactions and service-to-service failure handling before you have users. The companies now famous for microservices started as monoliths and split under real load, not anticipated load.

5. Default to relational, and demand evidence before leaving

For a CRM with customizable fields, the choice isn't as binary as SQL versus NoSQL. Postgres with JSONB columns handles flexible schemas while keeping ACID guarantees, joins, and mature reporting. Document databases earn their place when the data is genuinely schema-free at large scale or when your access patterns fit key-value reads. "It's expected to grow quickly" is not that evidence; relational databases scale much further than the folklore suggests. Choose NoSQL for a concrete access pattern it serves better, not for a growth projection.

6. Under pressure, take the reversible fix

When a recommendation query is melting the database and customers are complaining now, a Redis cache is the right first move: large gains, small disruption, and easy to remove later. Precomputing recommendations in a pipeline is probably the better end state, but it costs storage, new infrastructure, and time you don't have during an incident. The general principle is reversibility. When you must act fast, pick the option that's cheap to undo, stabilize, and then build the durable solution calmly.

7. Don't rewrite the legacy system

The full rewrite is the most seductive bad idea in software. Another reader shared the version of this story that almost always plays out: stakeholders wanted a 20-year-old critical system rebuilt from scratch, which would have meant freezing features for at least six months against a roadmap that made freezing impossible. They chose gradual refactoring instead, and it worked. The mechanics: find the modules that break most or change slowest, wrap them behind stable interfaces, extract or rebuild the low-risk high-impact pieces first, and invest in tests and CI so each step is safe. It's slower per module and dramatically faster to value, because the business never stops shipping while you work.

The habits behind good calls

Across all seven, a few habits repeat. Study your team's past decisions and post-mortems, because your own history is the most relevant dataset you have. Prototype before committing when the stakes are high; theory lies about performance. Weigh reversibility explicitly, since a wrong choice you can undo is barely a mistake and a wrong choice you can't is a crisis. And watch your own bias in both directions: excitement about new tech and comfort with old tech are both emotions, and neither is an architecture review.