Which architecture decisions should a startup get right in its first version? Not the ones about scale it doesn't have yet. The decisions worth making carefully in an MVP are the ones that are cheap to make now and expensive to change later: how the code is divided into domains, who owns which data, the contracts between parts of the system, what you can see in production, and how changes roll out and back. Microservices, sharding and multi-region deployments can usually wait until real evidence calls for them.
This guide explains each of the five choices, what "good enough" looks like at the MVP stage, and how to revisit them as the product grows.
Plans built around user-count milestones tend to guess at problems the product may never have. Reversibility is a better test. Swapping a cache library or a logging tool is usually a contained change. Changing how tenants' data is separated, or untangling modules that read each other's tables, can take months once customers and integrations depend on it.
So spend design time where reversal is costly, and keep everything else simple. A small team shipping one deployable application with clear internal boundaries is usually easier to run, debug and change than a set of services designed for traffic that hasn't arrived.
Start with one deployable application divided into modules named after business capabilities, such as catalog, orders, billing and accounts, rather than technical layers. Each module exposes a small public interface and keeps its internals private.
Make the boundaries real:
- Modules call each other through their interfaces, never by reaching into another module's classes or tables.
- Shared code is limited to genuinely generic utilities, not business rules.
- Automated checks, such as dependency rules in the build, fail when a boundary is crossed.
This gives you most of the design benefit of separate services without their operational cost. If a module later needs to scale, deploy or change independently, its boundary is already drawn, and extraction may require less restructuring. You still need to redesign transaction boundaries, latency and failure handling for distributed operation.
Figure 1 — Organize around business capabilities. Start with a modular application.
A useful default is one accountable module per business-owned table, with that module controlling writes. Other modules ask through the owner's interface or react to its events. This reduces direct data coupling, though shared workflows and transactions still need explicit design.
If you serve multiple business customers, decide the tenancy model early, because it shapes every query, migration and security review:
- Shared tables with a tenant identifier on tenant-owned rows: often simpler to run, but every query must filter correctly, so enforce the filter centrally rather than trusting each developer to remember it.
- A schema or database per tenant: potentially greater separation and simpler per-customer export, with more operational work. Separate schemas alone are not a security boundary; credentials, database grants and application authorization must enforce isolation.
Whichever you choose, test unauthorized cross-tenant reads and writes across APIs, caches, jobs, search indexes and exports. Decide early how schema changes ship, too: backward-compatible migrations that let old and new code run side by side make deployments and rollbacks much safer. And restore a backup on purpose before you ever need to.
Contracts are the agreements between parts of your system, and between your system and the outside world. Write them down:
- External APIs: describe them in a specification such as OpenAPI, decide your versioning policy before the first partner integrates, and make write operations idempotent so that a retried request doesn't create a duplicate order or payment.
- Events: if modules communicate through events, give each event a documented schema and an owner, and treat changes to it like API changes.
- Third-party services: wrap payment, messaging and similar providers behind your own adapter, to contain integration-specific code. Switching can still require data migration, token replacement, contract changes and end-to-end testing.
Clear contracts are also what make a later split safe. A documented interface limits caller changes when a module moves behind a network boundary, but remote calls introduce latency, partial failures and retry behavior that callers must handle.
For retriable writes, scope idempotency keys to the tenant and operation, bind them to the request payload and persist the outcome atomically. Reject reuse with different input, and test concurrent retries. Event consumers need duplicate handling and an explicit ordering policy too.
When something breaks in production, you need to see what happened without guessing. Build that in from the first deployment:
- Structured logs with a request or correlation ID that follows each request across modules and background jobs.
- Metrics for the few user journeys that matter most, such as sign-up, checkout or report generation: how many succeed, how many fail and how long they take.
- Traces for slow or failing requests, so you can see which step took the time.
- Alerts on symptoms users would notice, not on every internal warning.
Keep personal data out of logs, or mask it, and set retention limits. Observability also informs architecture: decisions about caching, queues or splitting a module should rest on what the data shows.
How you ship changes decides how often you can ship them safely. From the start:
- Run automated tests and deploy through a pipeline, not by hand.
- Use feature flags to separate deploying code from releasing a feature, so you can switch a feature off without redeploying.
- Release gradually where you can, to internal users or a small share of traffic first.
- Write down how to roll back each release, including what happens to data the new version has written.
Smaller releases limit exposure and make recovery easier to rehearse. A flag or old binary does not undo database writes, payments or messages. Preserve compatible schemas, plan reconciliation or compensating actions and delay destructive cleanup until recovery is verified.
Figure 2 — Observe each controlled rollout. Plan recovery for data too.
Revisit these five decisions at regular points, such as before a major launch or a funding round, and whenever the evidence changes. Signals worth acting on include a module that needs to scale or deploy on a different schedule, team boundaries that keep colliding in one codebase, or a tenant whose data or compliance needs differ from the rest.
The AWS Well-Architected Framework offers a useful structure for these reviews through its six pillars: operational excellence, security, reliability, performance efficiency, cost optimization and sustainability. It is a review lens and doesn't require AWS services. When a module does need to move out, the strangler fig approach of routing traffic gradually to the new component applies to your own young system as well as to legacy software; our guide to legacy modernization with the strangler fig pattern explains the steps.
Two earlier decisions shape all of this. Check whether a SaaS product already covers the need before building at all, using our framework for choosing between SaaS and custom software. And choose a stack your team can support for years; our comparison of Laravel and Node.js for enterprise APIs shows how to decide on evidence.
If you are planning a first version, our website design and development service can help you set up these five foundations without over-building.
- Amazon Web Services. AWS Well-Architected Framework: the six pillars used for architecture reviews.
- Amazon Web Services. Strangler fig pattern, AWS Prescriptive Guidance: incremental replacement behind a routing facade, with old and new components coexisting.
The five decisions and their recommended defaults are TecnoStack's engineering guidance, not requirements from AWS or any other vendor.




