Written by Rodrigo de Miguel
Why I didn't set up microservices for my SaaS
(and how that allowed me to launch sooner and help more)
At ADOPTA, I chose not to implement microservices because the problem wasn't scalability, but rather launching quickly with minimal cost and maintenance in a project maintained by a single person. A modular monolith gave me clear boundaries, better DX, and a focus on real impact: helping more shelters and facilitating more adoptions.
The problem was never technical
ADOPTA was born with a very clear idea: to make animal adoption easier and to encourage it.
It’s a free SaaS for both shelters and individuals. No paywalls, no premium plans, no MRR to optimize.
That completely changes the rules of the game.
The goal wasn’t to maximize revenue, but to maximize impact:
- More shelters using the platform.
- More real adoptions.
- Less operational friction for people who are already short on resources.
From day one, the main constraint wasn’t future scalability or architectural elegance. It was something much more basic and real:
We had to ship something usable as soon as possible, with the lowest cost and the least maintenance possible.
And that inevitably shapes every technical decision.
The team: an uncomfortable, but very common reality
It’s worth making this clear from the start, because it affects everything else.
ADOPTA wasn’t built by a “backend team”.
It was built by me alone.
At the beginning I started with a good friend (shout-out to Tomy 😊👋), fresh out of university:
- He did frontend.
- I did backend.
But that didn’t last long. The project continued, but the team became one person.
No DevOps.
No SRE.
No one to hand things off to when something broke.
This isn’t a complaint. It’s the real context from which sensible decisions are made.
The myth: “If it’s a SaaS, it has to be microservices”
There’s an automatic association you hear all the time:
SaaS = microservices = serious architecture
And it’s a dangerous association.
Because it mixes three different things:
- Business model
- Deployment style
- Internal design quality
A monolith is not synonymous with bad code.
A microservice is not synonymous with good code.
A monolith is simply all the code deployed as a single unit.
The monolith’s bad reputation comes from something else:
systems where nobody thought about architecture, boundaries, or maintainability. That’s not a monolith problem. That’s a technical judgment problem.
The real starting point: a classic (and honest) monolith
Here’s an important part of the learning:
ADOPTA did not start as a modular monolith.
It started as a classic monolith.
And it was a conscious decision.
At that moment:
- The domain was still immature.
- Features changed every week.
- There was no certainty about what would survive.
Over-optimizing the architecture too early would have been a waste of time.
The goal was:
- Understand the problem.
- Validate flows.
- See how shelters actually used the platform.
Learn, and nothing more.
Technical debt showed up (as it always does)
Over time, the inevitable happened:
- The code grew.
- Awkward dependencies appeared.
- Some quick decisions started to bite back.
But here’s the key difference:
Technical debt didn’t appear because we used a monolith.
It appeared because the product evolved.
That would have happened with microservices too, except:
- It would have been more expensive.
- Harder to change.
- Slower to understand.
The real technical fork: what to do about that debt
There was a clear decision point:
Option A: “Since we’re at it, let’s break it into microservices”
The classic temptation.
Problem:
- More complex infra.
- Higher operational cost.
- Distributed debugging.
- A lot of investment for a project with no economic ROI.
Option B: Refactor into a modular monolith
Less sexy. Much more pragmatic.
- One deployment.
- One runtime.
- Better internal design.
- Less daily friction.
I chose option B.
Important: I don’t use DDD (and that’s okay)
Another point worth clarifying because it often creates noise.
In ADOPTA:
- No formal Domain-Driven Design is used.
- No textbook aggregates, value objects, or bounded contexts.
What is used:
- Screaming Architecture: the project structure screams what the system does.
- MVC (Model-View-Controller) + SRP (Single Responsibility Principle):
- Independent domain.
- Clear layers.
- Replaceable infrastructure.
This was a conscious decision to avoid:
- Conceptual overhead.
- Unnecessary ceremony for a one-person project.
What “modular monolith” means here
It doesn’t mean pretty folders.
It means clear, respected boundaries, even if everything is deployed together.
Key principles
- Each functional area lives in its own module.
- The domain doesn’t know frameworks.
- Models aren’t shared “for convenience”.
- The database doesn’t dictate the architecture.
Conceptual example:
TEXT/adoptions controllers/ repositories/ services/ /shelters controllers/ repositories/ services/ /users controllers/ repositories/ services/ /shared
The difference from the initial monolith wasn’t technological. It was discipline.
Why microservices would have been a bad idea here
With a single person maintaining the system:
- Each microservice is another system to understand.
- Each deployment is another point of failure.
- Each distributed error costs more mental time.
And here’s a key sentence that sums up the decision:
Microservices are premature optimization when you don’t have a real scaling problem.
ADOPTA didn’t have throughput problems. It had time, focus, and energy problems (and staffing 🐣).
The hidden cost of microservices: everything becomes an interface
There’s a detail that sounds minor, but in practice it changes everything:
microservices don’t communicate via functions, they communicate via interfaces.
REST APIs, queues, events, topics, versioned contracts.
All of that is necessary in a distributed system, but it’s not free.
In a monolith, a flow is:
call a function
In microservices, that same flow is:
- serialize data
- send it over the network
- deserialize it
- handle timeouts
- deal with partial failures
- version contracts
That adds technical and cognitive complexity.
Developer Experience: when the code is the documentation
In a well-designed modular monolith:
- The documentation is the code itself.
- The IDE understands the whole system.
- IDE autocomplete guides you.
- Navigating call chains is immediate.
You can:
- jump to definition
- see real types
- refactor safely
You don’t need:
- Swagger to understand what’s happening
- Postman to test an internal flow
- external documentation to avoid breaking things
Everything is there—alive and coherent.
Remember: it’s a system maintained by one person who started out as a junior.
When the cost isn’t technical, it’s mental
With a single person maintaining the system, this is critical.
Each external interface adds:
- friction
- context you have to load into your head
- time lost understanding contracts
In a project like ADOPTA, where:
- there are no separate teams
- there are no internal SLAs
- there’s no real need for isolation
Using microservices would have made DX worse with no tangible benefit.
Fewer interfaces, more focus
Reducing the number of interfaces isn’t technical laziness.
It’s a very effective way to:
- reduce bugs
- speed up changes
- keep the system alive with less energy
And when the goal is social impact, not theoretical scalability, that focus matters more than any pretty diagram.
Infrastructure: less is more
ADOPTA’s infrastructure is deliberately simple:
- One backend.
- One deployment.
- One pipeline.
- One well-designed database.
None of this:
- Kubernetes.
- Service mesh.
- “Just in case” advanced observability.
Every extra piece would have been:
- More cost.
- More maintenance.
- More risk for an altruistic project.
The ROI here isn’t money (and that matters)
This is a fundamental point.
ADOPTA’s ROI isn’t measured in euros. It’s measured in:
- Number of adoptions facilitated.
- Time shelters save on management.
- Number of active shelters.
Choosing a simple architecture made it possible to:
- Keep costs low.
- Avoid relying on external funding.
- Sustain the project over time.
That’s ROI too, even if it doesn’t show up on a financial spreadsheet.
Technical debt, revisited with perspective
The refactor to a modular monolith was, in itself, paying technical debt.
But it wasn’t done:
- All at once.
- Or too early.
- Or for “purity”.
It was done when:
- The domain started to stabilize.
- The pain was real.
- The benefit was clear.
Well-managed technical debt doesn’t slow projects down. It speeds them up.
What could go wrong (and how to avoid it)
Risks of a modular monolith
- Relaxing boundaries.
- “Since we’re here” code sharing.
- Slowly slipping back into spaghetti.
Real mitigations
- Constant dependency review.
- Small, frequent refactors.
- Conscious decisions, not automatic ones.
What if one day we need to scale?
The inevitable question.
The honest answer is:
When that day comes, the system will be much better prepared than if it had started with microservices.
Because:
- The domains are clear.
- Dependencies are controlled.
- Extracting a module would be possible.
If you can’t do that, you didn’t have microservices. You had a distributed monolith.
My lesson
Architecture isn’t a complexity contest.
It’s a means to:
- Solve real problems.
- With real resources.
- In imperfect contexts.
Choosing a monolith (and evolving it well) isn’t a lack of ambition. It’s good judgment.
And that judgment is what allows projects like ADOPTA to exist, be maintained, and genuinely help.
Not building microservices wasn’t giving up. It was a conscious decision aligned with the project’s purpose.
ADOPTA exists today because the architecture didn’t eat the product.
And that, very often, is the most senior decision you can make.