How We Turned the Once-Single Omnilude Backend into an MSA with AI
Introduction
This article continues from This is the backend we are building for Omnilude. In the previous article, I introduced Omnilude's current backend structure and responsibility boundaries as if it were a single product backend. In this article, I want to explain how that structure actually started to split from one large project into multiple service boundaries.
MSA discussions often sound more abstract than they should. It is easy to say that a system was split, that deployments became independent, or that boundaries were defined. It is much harder to picture what really changed inside a live project.
Omnilude has enough concrete material to explain that change clearly. Today it is split into names like auth-service, backbone-service, storage-service, ai-service, game-service, blog-service, and legacy-service, but it did not start that way. At first, it was one large Spring Boot backend.
In this article, I will walk through why that single project started to split, why jspring(common) came first, why legacy-service was intentionally left in place, and what role AI actually played in the transition.
At first, it was just one big backend
Before the transition, Omnilude's backend was, put simply, a place where many things were piled into one project. Authentication was there. AI was there. Game logic was there. File storage was there. Real-time events were there. Common utilities were there. Everything lived inside the same codebase.
That kind of structure feels convenient at first. You can add features quickly and edit things in one place. But as the project grows, problems begin to show up.
- Areas like authentication that must be handled carefully
- Areas like AI that change often and invite experimentation
- Areas like real-time events where stability matters
- Areas like games where domain logic grows quickly
All of them start to share the same deployment pressure.
A simple way to picture it is a closet filled with seasonal clothes, gym wear, formal suits, blankets, and a toolbox all at once. At the beginning, it feels efficient. At some point, even changing one thing starts to feel heavy.
Why split it at all?
I was not chasing MSA as a goal by itself from the beginning. The reasons were much more practical.
The biggest reason was that I wanted to protect features that need long-lived connections from the impact of ordinary deployments. Omnilude had areas close to WebSocket, SSE, event streams, and long-running asynchronous work, especially around backbone-service. If those areas were shaken by unrelated domain changes, the effect was immediately visible in operations. A structure where a blog or game update could restart services that depend on connectivity was not something I wanted to keep.
The second reason was deployment time. Even a small change meant building the whole large application, turning it into an image, and shipping it again. As the project grew, that time felt more and more wasteful. If fast-changing areas are tied to one deployment unit, the whole system slows down together.
Then there was the issue of blurred responsibility boundaries. Authentication rules, common exceptions, internal HTTP, messaging, and file storage were mixed across the codebase, so it became harder and harder to tell what was common and what was domain logic.
There was also a bigger reason. I already knew what I wanted to build next. In Omnilude, I wanted to expand AI features, build up game services, and keep adding separate products such as the blog and tools. Continuing to grow everything inside one backend no longer looked like a good choice.
In other words, this transition was not something I did because MSA looked fashionable. It was much closer to restructuring the system so I could protect connection-sensitive services, shorten deployment time, and keep pushing bigger ideas forward.
The first step was not cutting services, but extracting common rules
There is an important point here. The first step in this transition was not pulling out auth-service or ai-service. Before that, I created common.
This matters quite a lot. From the outside, service decomposition looks like cutting domains apart. In practice, the first real job is identifying what the shared contract is and moving it out.
In Omnilude, common took that role.
- Common exception handling
- JWT conventions
- Shared DTOs
- Web-layer utilities
- JPA and data access foundations
- Internal HTTP and messaging helpers
Once those pieces were moved into common, later services could still speak the same language instead of turning into unrelated applications.
A simple analogy would be agreeing on the power sockets and door handles before dividing a building into multiple rooms. If those shared standards are not aligned first, dividing the rooms does not actually make living there easier.
The split happened surprisingly quickly
If you look at the repository history, the transition happened in a surprisingly short window. The main skeleton was shaped between January 18 and January 22, 2026.
In simple terms, the flow looked like this.
- Extract
common - Split
auth-service - Split
backbone-service - Split
storage-service - Start separating
ai-service - Isolate the remaining areas into
legacy-service - Extract
game-service - Put
blog-serviceon top as a new service - Reorganize Jenkins and Kubernetes around selective deployment
That order had its own logic.
auth-service had relatively clear boundaries. Authentication, accounts, and permissions are shared dependencies across other services, so pulling it out early created a strong reference point for the rest of the structure.
backbone-service had a stronger infrastructure flavor with real-time events, messaging, and DTE-related concerns. It made operational sense to separate it from faster-moving product logic.
storage-service was meant to own the shared contract for file storage. Once file handling sits in one place, the blog, the game, and AI-generated outputs can all follow the same path.
Then came ai-service. That area was harder because it was larger and had more dependencies. But it was also one of the places I expected to experiment with most often, so treating it as an independent service made sense.
Why keep legacy-service?
When people imagine an MSA transition, they often imagine a clean ending where the monolith disappears all at once. In practice, that picture is less realistic than it sounds.
That is why Omnilude grouped the domains that had not yet been fully extracted into legacy-service.
This decision was not really a retreat. It was closer to building a buffer.
legacy-service made several things possible.
- I did not have to break unfinished migrations by force.
- New services and old services could coexist under the same product domain.
- The team could keep moving while the system stayed operable.
- Harder domains could wait while easier ones were extracted first.
To me, this is one of the most realistic parts of an MSA transition. Real transitions are usually less about achieving a perfect split and more about creating service boundaries that can actually survive operation.
Game, blog, and AI became clearer on top of that
From that point on, more product-shaped services started to become distinct.
ai-service is not just a chatbot API. It carries responsibilities such as agent execution, workflows, generation pipelines, media generation, roleplay systems, and product agents. At that size, treating it as an independent service is far more natural.
game-service was close to the area I ultimately wanted to grow the most in Omnilude. As story quizzes, scenarios, and asset studio features were added, it became a domain-centered service that tied AI, storage, and authentication together.
blog-service is a little different. It was not simply a domain cut straight out of the original monolith. It was closer to a service that found its place on top of an already separated architecture. Because deployment and shared rules were already split, the blog could be designed and operated as an independent service from the beginning.
That is why the MSA transition was not just about pulling old things apart. It also became the groundwork for launching new products in better units.
The moment it really changed was deployment
From code alone, a project can look like it has been split. But the point where MSA becomes real is when deployment changes.
In Omnilude, that is recorded in Jenkinsfile, Jenkinsfile.prd, and kubernetes/services/**.
At the moment, Jenkins can selectively deploy these seven services.
auth-servicebackbone-servicestorage-serviceai-servicegame-serviceblog-servicelegacy-service
That means the repository may still be one repository, but it is no longer one application that must always be deployed together. It became a structure where only the service you need can be released.
Kubernetes follows the same direction. Each service owns its own Deployment and Service, while Ingress routes requests under api.omnilude.com or dev-api.omnilude.com by path prefix.
That point matters. MSA is not about splitting folders. It is about splitting operating units. In Omnilude, it was only after crossing that line that the transition started to feel truly real.
What mattered in the end was not the number of services
After a transition like this, it is easy to focus on how many services were created. Looking back, the number was not the important part.
These four questions mattered much more.
- Did we pull out shared rules first so later separation was even possible?
- Did we distinguish fast-changing domains from slower infrastructure concerns?
- Did we create boundaries that were operable even if they were not perfect?
- Did we connect code separation to deployment separation in reality?
Seen through that lens, Omnilude's transition feels very practical to me. It may not be the cleanest diagram in the world, but it created a structure that can keep supporting the next product and the next experiment.
Closing
Omnilude's MSA transition was not really the result of a grand strategy document from day one. It was much closer to the kind of structural cleanup you eventually have to go through if you want to build something bigger.
It started as one large project, extracted common, split auth, backbone, storage, ai, game, and blog, kept the remaining areas in legacy-service, and finally aligned the real deployment units with Jenkins and Kubernetes.
To me, that feels very Omnilude. The name Omnilude carries both the scope suggested by omni- and the playful tone suggested by -lude. It points to the idea of many functions and services not scattering forever, but eventually gathering into one larger experience of play. In that sense, this transition resembles the name itself: the destination was clear, so the structure kept changing to fit it.
In the next article, I plan to go one step further from architecture and talk about how I am actually implementing the game.