One of the most interesting architecture decisions I worked through was not about choosing between two technologies.
It was about deciding how much architecture we actually needed.
We had a large Java/Spring application that had been running for several years. Like many enterprise applications, it had grown organically. New functionality had been added, existing functionality had evolved, and over time the boundaries inside the application had become less clear.
The application was still delivering business value. It wasn’t a system that needed to be thrown away.
But one particular business capability had started creating problems.
It had much higher transaction volume than the rest of the application. It changed more frequently, had different performance characteristics, and occasionally required significantly more resources during peak periods.
The deployment model, however, hadn’t changed.
Everything was still packaged and deployed together.
That created a simple but increasingly painful mismatch:
One part of the system was evolving and scaling differently, but the architecture treated the entire application as one unit.
The initial proposal from the team was predictable:
“We should move to microservices.”
It was a reasonable proposal.
But I didn’t think the answer was to turn the entire application into microservices.
The interesting part was figuring out why.
1. Start With the Problem, Not the Architecture
The first thing I wanted to establish was what problem we were actually trying to solve.
It would have been easy to start a discussion around:
“Should we migrate to microservices?”
Instead, I reframed the discussion:
“What is failing in the current architecture, and what capability do we need that we don’t have today?”
Once we looked at the problem that way, it became much clearer.
We had two significant issues.
The first was scaling.
The high-volume capability needed more capacity during certain periods, but because everything was deployed together, scaling that capability meant scaling the entire application.
The second was deployment.
That capability changed much more frequently than the rest of the system. A change that affected one area still required the entire application to go through the same deployment process.
So the actual requirements were:
- Independent scaling for one business capability
- Independent deployment for that capability
- A clearer ownership boundary
- Reduced impact on the stable parts of the application
Notice what wasn’t on the list.
We didn’t have a requirement saying:
“We need 20 microservices.”
We didn’t have a requirement saying:
“Every business capability must be independently deployable.”
And we certainly didn’t have a requirement saying:
“The application must become cloud-native.”
Those are architectural approaches, not business problems.
That distinction became the foundation of the decision.
2. The First Option: Optimize the Monolith
The first option was to leave the architecture largely as it was and optimize the problematic area.
We could improve database queries, caching, JVM configuration, connection pools, thread pools, and application performance.
There were certainly optimizations worth doing.
And I didn’t dismiss them.
If a system is slow, you should understand why before changing its architecture.
But optimization couldn’t solve the fundamental problem.
Even if we made the capability significantly faster, it would still be part of the same deployment.
If that capability needed more instances, the rest of the application would still be scaled along with it.
If that capability needed a release, the entire application would still go through the release process.
So optimization could improve performance, but it couldn’t provide independence.
That was an important distinction.
3. The Second Option: Build a Modular Monolith
The next option was more interesting.
Instead of jumping directly to microservices, we could first turn the existing application into a properly structured modular monolith.
This was attractive for several reasons.
The codebase had accumulated coupling over time.
We could establish stronger boundaries around business capabilities, make dependencies explicit, and prevent modules from reaching into each other’s internals.
A structure such as:
Application
│
├── Customer
├── Orders
├── Payments
├── Inventory
└── Reporting
could be enforced architecturally rather than relying on developers to follow conventions.
This would make the system easier to understand and maintain.
And, importantly, it would give us better boundaries if we later decided to extract another service.
So I considered the modular monolith a good architectural improvement.
But it still didn’t solve the core problem.
The application would remain one deployable unit.
The high-volume capability couldn’t be scaled independently.
The frequently changing capability couldn’t be deployed independently.
That led to an important conclusion:
Modularity was necessary, but it wasn’t sufficient.
We needed a stronger boundary around the specific capability.
4. The Third Option: Move Everything to Microservices
The obvious alternative was to go in the opposite direction.
If one capability needed independent deployment and scaling, why not make every capability independently deployable?
This was the strongest argument for microservices.
We would get:
- Independent deployments
- Independent scaling
- Clear service ownership
- Smaller deployment units
- Potentially smaller failure domains
Technically, it could solve the problem.
But it would also introduce complexity across parts of the application that didn’t actually have the problem.
Once the application became distributed, we would have to deal with:
- Network failures
- Timeouts
- Retries
- Distributed tracing
- Service-to-service security
- API compatibility
- Contract testing
- Configuration management
- Data ownership
- Eventual consistency
- More complicated deployment pipelines
- More monitoring and operational responsibility
And there was another consideration that is sometimes overlooked in architecture discussions:
The organization has to be able to operate the architecture.
It is one thing to build a few Spring Boot services.
It is another thing to operate a large number of services reliably in production.
You need the engineering practices, deployment automation, observability, ownership model, and operational discipline to support it.
The architecture has to work not only on the whiteboard, but at 2 a.m. when something breaks.
5. The Decision: Extract Only What Needed Independence
After looking at the alternatives, the decision became much clearer.
We didn’t need to convert the entire application to microservices.
We needed to give one business capability the architectural characteristics it actually required.
So the decision was:
Keep the majority of the application together, but extract the high-volume, high-change capability into an independently deployable service.
This gave us a hybrid architecture.
The existing application remained largely intact:
┌──────────────────────────┐
│ Existing App │
│ │
│ Customer │
│ Payments │
│ Inventory │
│ Reporting │
└────────────┬─────────────┘
│
API / Events
│
┌────────────▼─────────────┐
│ Extracted Service │
│ │
│ High-volume Capability │
└──────────────────────────┘
The important thing was not the diagram.
It was the principle behind it:
Introduce a service boundary where independent deployment and scaling provide real value.
We weren’t trying to maximize the number of services.
We were trying to create the right boundary.
6. Why This Was a Better Trade-off
The decision wasn’t without cost.
We were deliberately introducing distributed-system complexity.
Previously, the application could make a method call.
Now, some interactions would cross a network boundary.
That meant we had to think about failures that didn’t exist inside the monolith.
A service could be unavailable.
A request could time out.
A response could be delayed.
A message could potentially be delivered more than once.
A deployment could create an API compatibility problem.
Data could become temporarily inconsistent.
So why accept all of that?
Because this time, the complexity had a reason to exist.
The service needed to scale independently.
It needed to be deployed independently.
The business capability had become sufficiently different from the rest of the application that a separate operational boundary was justified.
That was the trade-off.
We weren’t eliminating complexity.
We were spending complexity where it bought us something valuable.
7. Finding the Right Boundary
Extracting a service is much easier when the boundary is based on business responsibility rather than technical structure.
We didn’t want to take a Java package and simply turn it into a Spring Boot application.
That would have created a new deployment unit without necessarily creating a meaningful business boundary.
Instead, we looked at:
- Which business responsibilities belonged to the capability
- Which data it owned
- Who consumed it
- Which other capabilities it depended on
- Which dependencies could be removed
- Which interactions genuinely needed to cross the boundary
The goal was to make the service responsible for a coherent business capability.
A good service boundary should allow the service to evolve without constantly reaching back into the internals of the monolith.
If every operation requires a trip back into the old application, the boundary isn’t really independent.
8. Data Ownership Was a Critical Decision
One of the easiest ways to create a distributed monolith is to split the application into services while keeping the same database coupling.
For example:
Service A ──────┐
│
Service B ──────┼──── Shared Database
│
Monolith ───────┘
At first glance, this looks like microservices.
But the services are still coupled through the database.
A change to one service’s data model can affect another service.
A deployment may still require coordination.
The database becomes the hidden integration layer.
That’s not the independence we were trying to achieve.
The extracted capability therefore needed clear ownership of its data.
Where information needed to move between the existing application and the new service, that interaction had to be explicit.
This is one of the most important principles in service decomposition:
A deployment boundary without a data boundary is often not a real architectural boundary.
9. Synchronous vs. Asynchronous Communication
Not every interaction needed to become asynchronous.
This is another area where architectural discussions can become overly ideological.
There is nothing inherently wrong with synchronous communication.
If a caller genuinely needs an immediate answer, a synchronous API is often the simplest solution.
For operations that didn’t require an immediate response, asynchronous messaging could be used.
The decision should be driven by business semantics.
For example:
“Create the payment and return the result.”
is fundamentally different from:
“Record that this event happened and process the downstream work when appropriate.”
The first may naturally require synchronous communication.
The second may be a good candidate for asynchronous processing.
The important lesson is:
Don’t introduce asynchronous messaging just because you are building microservices.
Messaging itself introduces complexity.
Use it when the business process benefits from it.
10. Migration Was More Important Than the Target Architecture
Another important decision was not to perform a big-bang migration.
The existing implementation couldn’t simply disappear overnight.
The new service had to be introduced gradually.
The migration therefore followed an incremental approach.
The new capability was built and validated first.
Then integration between the existing application and the new service was introduced.
Traffic could be moved progressively, with monitoring and rollback options available.
This approach had a major advantage:
We could learn while migrating.
If performance wasn’t what we expected, we could investigate.
If an integration assumption was wrong, we could fix it.
If data consistency created an unexpected problem, we could address it before moving more traffic.
A migration isn’t just an implementation exercise.
It is a risk-management exercise.
The safest architecture migration is often the one that gives you the most opportunities to discover that you’re wrong before the entire system depends on the new design.
11. Observability Became a First-Class Concern
Once the capability was separated into another process, debugging changed.
Inside a monolith, a request could often be followed through one application.
With a service boundary, the request could move across multiple processes.
That meant we needed proper observability.
Logging, metrics, health checks, correlation IDs, and tracing became important parts of the architecture.
For example:
Request
│
▼
Monolith
│
│ correlation-id
▼
Service
│
▼
Database
When something failed, we needed to answer questions such as:
- Where did the request fail?
- Was the service slow or unavailable?
- Did the database cause the delay?
- Was the request retried?
- Did a downstream dependency fail?
- Was the problem inside the monolith or the extracted service?
Distributed architecture without observability is a dangerous combination.
You don’t want to discover that during a production incident.
12. The Anti-Pattern We Wanted to Avoid
The biggest architectural risk was creating what is often called a distributed monolith.
A distributed monolith has multiple services but remains tightly coupled.
Some common symptoms are:
- Services sharing database tables
- Services that must be deployed together
- Excessive synchronous service-to-service calls
- Long chains of dependencies
- No clear data ownership
- Changes requiring coordinated releases
- One service being unable to function without several others
This architecture can be worse than the original monolith.
The original monolith at least has the advantage of being inside one process.
A distributed monolith adds:
- Network latency
- Network failures
- Deployment complexity
- Monitoring complexity
- Distributed debugging
without necessarily providing true independence.
The goal of decomposition should therefore not be:
“How do we create more services?”
It should be:
“Where do we need genuine independence?”
13. Why the Modular Monolith Still Mattered
There is an interesting part of this decision that is easy to miss.
Choosing selective decomposition did not mean abandoning the modular monolith idea.
Quite the opposite.
The remaining application still benefited from strong internal boundaries.
The service extraction solved the deployment and scaling problem.
Modularization helped solve the code organization and coupling problem.
These approaches aren’t mutually exclusive.
A practical architecture can look like:
System
│
┌──────────┴──────────┐
│ │
▼ ▼
Modular Monolith Independent Service
│ │
Internal Modules Own Data
│ │
└──── API / Events ───┘
This is an important lesson.
Architecture isn’t a menu where you have to select exactly one pattern.
You can combine patterns when they solve different problems.
14. What Changed After the Decision?
The value of the decision wasn’t that we could now say:
“We have microservices.”
That wasn’t the goal.
The value was that the architecture better matched the characteristics of the system.
The high-volume capability could scale independently. During peak periods, we scaled the service from 3 instances to 12, while the monolith remained unchanged. Throughput increased by approximately 4x without impacting the other parts of the application.
The high-change capability could also be deployed independently. Deployment frequency went from weekly releases for the entire monolith to multiple deployments per day for that service. Deployment time dropped from approximately 45 minutes for the full application pipeline to around 8 minutes for the extracted service.
The stable parts of the application no longer needed to participate in every change to that capability. More importantly, the blast radius of failures was reduced. Problems in the extracted service could be isolated without necessarily affecting the rest of the application.
We tracked these results over six months.
The numbers weren’t the goal. They were evidence that the architectural trade-off was justified.
And perhaps more importantly, the organization could continue modernizing incrementally rather than committing to a massive rewrite.
The architecture became more adaptable.
That was the real outcome.
15. What I Would Not Do
There are a few lessons from this decision that are worth stating explicitly.
I would not start by deciding that the application needs microservices.
First understand the problem.
I would not extract services simply because the codebase is large.
Size alone isn’t a sufficient reason for service decomposition.
I would not create a service that depends heavily on the monolith’s database.
That usually creates a distributed monolith.
I would not introduce asynchronous messaging everywhere.
Messaging is useful, but it also creates its own complexity.
I would not perform a large rewrite when an incremental change can solve the immediate problem.
Migration risk is an architectural concern.
And I would not treat the number of services as a measure of architectural maturity.
Ten well-designed services aren’t necessarily better than three.
And three aren’t necessarily better than one.
16. The Architectural Lessons
Looking back at the decision, several principles stand out.
1. Start With the Problem
The question wasn’t:
“Should we use microservices?”
It was:
“Why can’t this capability scale and deploy independently?”
That question led us to a much more focused solution.
2. Architecture Is About Trade-offs
Every architecture gives you something and takes something away.
Microservices give you deployment and scaling independence.
They also give you distributed-system complexity.
A monolith gives you simplicity of deployment and communication.
It can also create scaling and deployment coupling.
There is no free architecture.
3. Complexity Should Be Justified
Distributed systems are powerful.
But they should be introduced because the business or operational requirements justify them.
Not because they are fashionable.
4. Boundaries Matter More Than Labels
Calling something a microservice doesn’t make it well designed.
The quality of the business boundary, data ownership, and communication model matters much more than the terminology.
5. Incremental Architecture Is Powerful
You don’t need to predict the final architecture.
Create one boundary.
Measure the result.
Learn.
Then decide what comes next.
That allows architecture to evolve based on evidence rather than assumptions.
17. Final Thought
The most interesting architectural decisions are rarely about choosing between a good solution and a bad solution.
They are about choosing between multiple reasonable solutions with different trade-offs.
In this case, a full microservices migration could have solved the problem.
A modular monolith could have improved the internal structure.
Optimizing the existing application could have improved performance.
But none of those options, by themselves, matched the actual problem as well as selective decomposition.
The final decision was therefore not:
Microservices are better than monoliths.
It was:
This particular capability has earned the complexity of being a separate service. The rest of the application hasn’t.
That distinction is at the heart of good architecture.
Don’t optimize for architectural fashion.
Don’t optimize for the number of services.
Optimize for solving the problem with the right amount of complexity.