← BLOGHOME↗

Apr 22, 2025 · 5 min read · Backend Engineering and Architecture

Splitting Part of a Monolith Into Services

Breaking a large application into services gave us clearer boundaries and more independent deployments. It also added network failures, distributed data and more operational work.

The first service we separated from a large application solved a real problem. It also gave us a new API to version, another deployment to monitor, and a network call that could fail. That trade-off shaped the modernization work I participated in at Softcraft. The existing system had real problems. Deployments carried unrelated changes, modules were coupled, and understanding the effect of a feature often required too much context. Separating responsibilities helped. Teams could work on some areas more independently, and changes no longer required deploying the entire application every time. The complexity didn’t disappear. Part of it moved from the codebase to communication, data ownership and operations.

From a Method Call to a Network Call

Inside the monolith, one part of the application could call another in the same process. The call was fast, and a shared transaction could keep related changes together. After separation, that interaction crossed a network. Now the other service could be unavailable. The request could time out even if the work completed. Contracts needed versioning. Authentication had to work between services. Logs from one operation were spread across applications. For some boundaries, these costs were worth it. We just needed to include them in the decision. Calling the architecture “microservices” didn’t solve them. We needed explicit decisions about retries, ownership, observability, and what should happen when only part of a workflow succeeded.

The first version of a new service still depended on the monolith for part of the operation. We introduced the API behind an existing application boundary so consumers didn’t all need to change at once, and we passed an operation identifier across both sides. During the transition, support needed to know if a request had remained in the monolith, reached the service or completed there but failed while returning the result. Without that context, the network boundary would have made an already difficult workflow harder to investigate.

We also had to decide what the caller should do during an outage. Some operations could wait and retry later. Others needed an immediate answer because the user couldn’t continue without it. We didn’t place the same retry behavior around every call. A request that changed business state needed more care than a read that could safely run again.

Data Ownership

The monolith made it easy for different modules to read the same tables. That convenience also created coupling. A change that looked local could break a query in another part of the system. Service boundaries forced us to discuss which capability owned the data and how other parts of the platform should access it. Those conversations improved the architecture because responsibilities became visible. They also introduced distributed consistency. Not every workflow could rely on one database transaction anymore. Some information would be updated later, and the system needed to represent that state honestly. The ownership became clearer, but the consistency between systems became more complex.

Moving the code was faster than moving the ownership. For a period, one service still read tables managed by the monolith because changing every consumer and data flow in the same release was too risky. We treated that as a migration state and kept track of which dependency we still needed to remove. In the next steps, the service became responsible for its own data and shared changes through contracts rather than allowing other applications to query its tables directly.

That created cases where information was briefly different between systems. For the workflows that could tolerate it, we made the delay visible and allowed the data to converge. Where the operation required a stronger guarantee, we kept the relevant work together instead of distributing it only to make the architecture look consistent. This was one reason some modules remained inside the application.

Independent Deployments

A separate repository didn’t make a service independent. It still needed a repeatable build, deployment, configuration, monitoring, and recovery process. If every service required manual coordination or a different operational approach, the organization only had more deployment units. Our modernization work was connected to improvements in CI/CD, containerization, automated testing, and observability. Without that foundation, additional services would have multiplied operational friction. The team needed to be able to operate the architecture we designed. Some parts of the application benefited from separation because they had clear responsibilities, different scaling needs, or integration workloads that needed independent processing. Other parts were better kept together, where a good internal boundary improved maintainability without adding a network call, another deployment, and another production surface.

Independent deployment also required contract compatibility. A service could be released without the monolith only if both versions could communicate during the rollout. We avoided changes where one side needed the new version of the other at exactly the same moment, and we kept older fields or behavior during migration periods when consumers couldn’t all move together. The additional compatibility work was part of the price of releasing the components separately.

The teams needed ownership after the extraction as well. It wasn’t enough to say that a service belonged to one group if alerts still went to somebody else or only the original developer knew how to recover it. We added dashboards, runbooks and deployment information while the context was still fresh. These things weren’t the reason to create the service, but missing them would have made the new boundary dependent on people instead of code.

We never made one “monolith or microservices” decision for the whole platform. Some responsibilities became services because the team needed independent delivery or processing. Others stayed as modules because a network boundary didn’t give us enough in return. The final picture was less consistent than a diagram where every box followed the same rule, but it reflected the problems we had. It also left us with fewer deployments and failure paths than separating everything would have created.

Some extracted areas became easier to deploy and scale. Other workflows took more effort because we now had to follow them across applications. After operating both, I started asking which coordination problem a new service would remove and whether that was worth another failure path.