Architecture for a team of three
Architecture advice is usually written by people at large companies for people at large companies. For a team of three it is not merely excessive — it is a reliable way to stop shipping. Here is what works at this size.
The real price of microservices
On a diagram microservices are neat boxes. In life each box is a separate deploy, a separate log, a separate set of environment variables, a separate migration and a separate way to break at three in the morning. Plus the network between them, which can now answer with a timeout where there used to be a function call.
For a team of three that means spending most of the week operating infrastructure instead of building the product. Microservices solve an organisational problem: several teams blocking each other on releases. With one team you do not have that problem, but you pay the full price.
The modular monolith
What works is one application split internally into modules with explicit boundaries. One deploy, one database, one log. But the code is arranged so a service can be extracted when there is a reason, not a preference.
Signs the boundaries are real rather than drawn:
- A module has a public entry point and does not let you reach into its internals.
- Modules do not import from each other except through a declared API.
- Each module owns its tables. Neighbours read them through a service, not through a JOIN.
- You can read the schema and tell which module a table belongs to.
That last one is the cheapest test. If three modules write to the same table, the boundary is not where you think it is.
One package of contracts
The biggest day-to-day pain in a small team is a frontend and a backend that quietly disagree about the data. Documentation does not fix it; a shared package of types and validation schemas that both sides depend on does.
In practice: one zod schema per rule, the TypeScript type inferred from it, the backend turning it into a DTO and the frontend using it for form validation. The rule is written once. Change it and both applications stop compiling — which is exactly right. The error should surface at build time, not in production.
Duplicating a validation rule in two places is not code duplication. It is two different systems that happen to agree, for now.
One database, with rules
A shared database between modules is fine as long as three things hold:
- Constraints in the database, not only in code. NOT NULL, CHECK, unique indexes. Application validation protects the user; a constraint protects the data from the application.
- Indexes for real queries. Not "just in case", but the ones you can see in the slow query log.
- Soft delete where the data matters.
deletedAtinstead ofDELETEturns a catastrophe into a misunderstanding.
The minimum operations that pay off
A small team cannot run a full SRE stack, but four things pay for themselves almost immediately:
- An audit log. Who changed what, when. The first question in any data incident.
- A background queue. Mail, webhooks and heavy work outside the user request. Otherwise an SMTP outage becomes a broken contact form.
- An outbox. The message is written to the database in the same transaction as the event, and a worker delivers it. The lead survives even if mail is down.
- A backup with a restore test. A backup that has never been restored is a file of unknown purpose.
When to split after all
Extract a service when at least one of these appears:
- Part of the system has a fundamentally different load profile (video processing next to a contact form).
- Part of it has different availability or compliance requirements.
- A second team has appeared and releases are blocking each other.
Note that none of these sounds like "it is more correct this way". If the modules inside the monolith are honest, extracting a service is moving a folder and adding an HTTP layer. If the boundaries were drawn rather than enforced, no number of services will help.
In short
Three people is not "a small version of a big team". It is a different mode, where every unit of infrastructure takes time away from the product. A modular monolith, shared contracts, database constraints and four operational basics give you most of the benefits of grown-up architecture at a fraction of the cost.
- Microservices solve an organisational problem a small team does not have
- Module boundaries are only real when each module owns its tables
- One zod schema per rule: a contract break should fail the build
- Audit log, queue, outbox and a tested backup pay for themselves quickly
When releases block each other because of the number of teams, or when part of the system has a fundamentally different load profile. Code size alone is not the problem.
Yes, if the modules have honest boundaries: their own tables, a public service at the entrance and no direct imports into internals. Then extraction is a folder plus an HTTP layer.
