TL;DR
- With 10 users, microservices are almost always an expensive distraction. The normal problem is still product, sales, onboarding and data, not scalability.
- The right alternative is usually a modular monolith with PostgreSQL 16, asynchronous queues and well defined internal boundaries.
- Microservices only start to make sense early if there is regulatory isolation, very different workloads, independent teams or external integrations with high operational risk.
- The real cost is not “having more repositories”. It is observability, deployments, distributed tests, authentication between services, API versioning, incidents and data consistency.
- Before migrating, measure: p95 and p99, throughput, deployment time, error rate, job duration, cost per environment and time spent diagnosing incidents.
The short answer: no, except in rare cases
If you have 10 users, the practical answer is: it is not worth migrating to microservices.
Not because microservices are bad. They are a useful tool when organisational and operational complexity already justifies separation. The problem is using them before the symptoms they solve exist.
With 10 users, the most likely risk is not that the database will not scale. It is that the product does not yet have market fit, the team does not yet know which flows are critical, the domain is still changing every week and the requirements have not yet stabilised. In these scenarios, distributing the system across several services only makes every change slower.
At Impact Origin, when we look at early stage projects, including recent active clients with around two months and billing still to be defined, the question is rarely “how do we split this into services?”. The right question is tougher: “which part of the system has already proved that it deserves its own complexity?”. Often, none of it.
There are also projects where the stack is not even formally stable yet. That is not bad. It is normal in early stages. But if you are still deciding on technology, data model, payment flows, ERP integration or permissions logic, moving to microservices is skipping three steps.
The strong opinion: if you cannot yet clearly explain the boundaries of your domain on an A4 sheet, you should not be creating microservices. You should be clarifying the product and modularising the monolith.
The real cost of microservices
Many people think of microservices as “splitting the application into small parts”. In practice, it means replacing local calls with network calls, simple transactions with eventual consistency and predictable bugs with partial failures.
A call within the same process can take microseconds. An internal HTTP call, even within the same cloud region, can easily be between 5 ms and 50 ms. With TLS, authentication, retries and JSON serialisation, the p99 can reach 100 ms or more during periods of load. If a page needs 5 services to respond, you have just created a latency cascade.
The cost also appears in these areas:
| Area | Modular monolith | Microservices |
|---|---|---|
| Initial deployment | 1 pipeline | 3 to 10 pipelines |
| Local debugging | Start one app and one database | Orchestrate several services, queues, mocks and dependencies |
| Tests | Direct integration | Contract testing, fragile end-to-end tests, shared environments |
| Data | Simple ACID transactions | Events, idempotency, reconciliation, eventual consistency |
| Observability | Logs and metrics per app | Distributed tracing, log correlation, metrics per service |
| Security | Session and permissions in one place | Service-to-service authentication, scopes, secret rotation |
If you use Kubernetes, read its own official documentation before putting it into a small product: https://kubernetes.io/docs/home/. Kubernetes solves real problems, but it creates a large operational surface. Namespaces, ingress, probes, autoscaling, secrets, network policies, RBAC and cluster upgrades are not free in terms of time.
With 10 users, many teams would be better off with a well configured VM, Docker Compose for simple environments, managed PostgreSQL and a decent CI/CD pipeline. For example: GitHub Actions, Render, Fly.io, Heroku, Railway or AWS App Runner can be more than enough, depending on compliance and network requirements.
The point is not “never use complex cloud”. The point is: do not buy operational complexity before you have the load, revenue or risk to pay for it.
What you should build instead
The architecture I recommend for most small B2B SaaS products is a modular monolith.
This does not mean a ball of mud. It means a single application, with clear internal boundaries. For example:
- accounts and organisations module
- billing module
- permissions module
- integrations module
- notifications module
- reporting module
- asynchronous tasks module
The base can be Node.js 22 LTS with Fastify or NestJS, Ruby on Rails 8, Django 5, Laravel 11.NET 8 or Phoenix 1.7. The technology matters less than boundary discipline.
For data, PostgreSQL 16 goes a very long way. You have transactions, partial indexes, JSONB when necessary, Row Level Security, materialized views and useful extensions. The official documentation is excellent: https://www.postgresql.org/docs/16/.
For jobs, you can start with a simple queue. BullMQ with Redis is common in Node.js. pgmq is interesting when you want to reduce infrastructure and use PostgreSQL as a queue. If volume is low, for example 1,000 to 50,000 jobs per day, you do not need Kafka. Kafka is great when you have serious streams, multiple consumers and event retention as a requirement. For sending emails, generating PDFs, synchronising with an ERP or processing webhooks, a normal queue solves the problem.
A rule that works well: keep a single deployment, but prevent uncontrolled internal coupling.
You can do this with simple conventions:
- each module has its own folder, its own services and its own data repositories
- modules do not directly access each other’s tables without going through an internal interface
- internal events are explicit, even inside the monolith
- jobs are idempotent from the start
- permissions are centralised, not spread across controllers
This gives you a huge advantage: when a module proves that it deserves to exist as a separate service, you already have a conceptual boundary. Extraction becomes a technical decision, not emergency surgery.
When microservices can make sense even early on
There are exceptions. Few, but real.
The first is regulatory or data isolation. If you have a component that handles sensitive clinical data, critical financial data or information with different legal requirements, it may make sense to isolate it. In healthtech and fintech, this conversation appears early. Not because of performance, but because of risk, audit and minimum necessary access.
The second is highly asymmetrical load. Imagine an application with 10 human users, but which receives 2 million webhooks per month from an external platform. The user count is misleading. The system that processes events may need scale, retries, dead letter queues and rate limiting long before the administrative interface needs them.
The third is a different lifecycle. If you have a calculation engine, image processing, large file ingestion or ERP synchronisation that changes and scales independently, it may make sense to separate it. Even so, it often starts as a separate worker in the same repository, not as a microservices ecosystem.
The fourth is the team. Microservices are more about teams than computers. If you have a team of 20 engineers distributed across different domains, separation can reduce conflicts. If you have 2 or 3 people, it usually increases coordination.
The fifth is a dangerous external dependency. Integrations with ERPs, CRMs, payment gateways and eCommerce platforms can fail in unpleasant ways. In these cases, isolating a connector can be useful. Even so, isolation does not always require a microservice. It can be a queue, an outbox table and a well designed worker.
The common trap: splitting the database too early
The worst migration to microservices is the one that starts with the database.
I have seen this pattern in production: the team separates “users service”, “billing service” and “notifications service”, but they all continue to need the same data. The result: either they share the same database, violating service independence, or they duplicate data without a decent synchronisation strategy.
Both options hurt.
A shared database between microservices seems practical at first. But if service A reads tables from service B, you do not have microservices. You have a distributed monolith, which is worse than a normal monolith. Now you have the costs of network and independent deployments without real independence.
Separate databases require events, contracts, idempotency and reconciliation. For example, if billing needs to know the user’s email, will you copy that data? Will you query the users service at runtime? Will you react to a UserEmailChanged event? What happens if the event fails? How do you correct divergences?
The outbox pattern is a serious answer to this problem. Instead of saving data and publishing an event in two separate operations, you save the event in the same transaction as the main change. Then, a worker publishes the event. Martin Fowler describes related patterns well at https://martinfowler.com/articles/201701-event-driven.html, and the idea also appears in event-driven architectures used with Kafka, RabbitMQ or managed queues.
But notice: if you are already talking about outbox, retries, dead letters, idempotent consumers and reconciliation, you have to ask whether these mechanisms are justified for 10 users.
In many cases, the more mature decision is to keep a single database, with schemas or conventions by domain, and prepare the ground. PostgreSQL handles much larger volumes than most startups reach in their first years. With correct indexes, measured queries and connection pooling, 300 requests per second is not fiction in a well written monolith. Your bottleneck with 10 users will not be that.
How to decide: a simple and brutal grid
Before migrating, answer these questions with data, not anxiety.
| Question | If the answer is “no” |
|---|---|
| Do you have p95 above 300 ms on critical endpoints because of a specific module? | Do not migrate. Optimise queries, cache and jobs. |
| Do you have p99 above 1 s because of blocking internal calls? | First remove unnecessary calls and measure. |
| Do you have more than 5 deployments per day blocked by conflicts between teams? | With a small team, the problem may be process, not architecture. |
| Do you have modules with 10x different scaling needs? | If not, a separate process may be enough. |
| Do you have legal requirements that force isolation? | If not, avoid fragmenting data early. |
| Do you have minimum observability with structured logs, metrics and tracing? | Without this, microservices will make incidents worse. |
Minimum metrics before considering microservices:
- p50, p95 and p99 per endpoint
- 4xx and 5xx error rate
- average and maximum job time
- database size, for example 10 GB, 100 GB or 1 TB
- number of active connections in PostgreSQL
- monthly cost per environment
- average time to diagnose an incident
- deployment and rollback time
If you do not have these numbers, you are deciding architecture by feel. That is expensive.
For observability, OpenTelemetry is a good foundation today. The official documentation is at https://opentelemetry.io/docs/. You do not need a hugely expensive platform in the first month, but you do need logs with correlation IDs, useful metrics and traces in critical flows.
You should also look at API contracts. If you start separating services, define OpenAPI 3.1 for HTTP or Protobuf for gRPC. Without a contract, every deployment becomes a gamble. The OpenAPI specification is at https://spec.openapis.org/oas/latest.html.
A sensible migration path
A good migration to microservices starts long before you create the first service.
First, clean up the monolith. Separate modules, remove circular dependencies, centralise permissions and create internal interfaces. If this is impossible, microservices will not solve it. They will only distribute the mess.
Second, move slow work to background jobs. An endpoint that generates reports, synchronises stock or calls external APIs should not block the user’s request. With 10 users, this change usually provides more gains than an architectural rewrite.
Third, create internal events. They do not need to leave the process on day one. The objective is to make it explicit that “payment confirmed” triggers “update subscription”, “issue receipt” and “send email”. When this is clear, you can replace the internal mechanism with a real queue.
Fourth, measure. If a specific module consumes 80% of the CPU, 90% of response time or requires constant independent deployments, you have a candidate. Otherwise, you only have a desire to tinker with architecture.
Fifth, extract a small service with an obvious boundary. Good candidates: file processing, notifications, webhooks, search engine, PDF generation. Bad initial candidates: users, permissions and billing. These tend to be connected to everything.
Sixth, accept the full cost. A new service needs a pipeline, health checks, logs, metrics, alerts, documentation, contract tests, secret management and a rollback plan. If you do not have the appetite for this, you do not have the appetite for microservices.
A practical rule: if you cannot operate two services confidently at 03:00, do not create ten.
Conclusion
With 10 users, microservices are rarely the right decision. A modular monolith, well measured and with asynchronous jobs, gives you more speed and less risk. Migrate only when there is data, clear boundaries and a strong operational reason.
If you are facing a similar problem, book a conversation at https://impact-origin.com/agendamento.