← Lessons |

What Microservices Actually Cost — Lessons From 13 Services

The Gap Between the Diagram and the Reality

Every microservices article shows you the same diagram. Boxes. Arrows. Clean separation. Services that scale independently, deploy independently, fail independently. The diagram is not wrong — those benefits are real. What the diagram doesn’t show is the operational surface area you’re signing up for when the service count gets into double digits.

XenTask is a 13-service architecture: API gateway, frontend, login, WebSocket server, forms engine, admin interface, admin API, core library, and more. Building and running it taught me things that only show up at scale.

Shared Code Across Services Reveals Whether You Need a Monorepo

Every service in XenTask uses a shared PHP library — database access, session management, logging, the inter-service HTTP client. The way this was solved was a git submodule: one ubcore repo, pulled into every service that needed it.

This works. It also generates a specific kind of friction that compounds with service count.

Change anything in the shared library and you now have a cascade: update the submodule pointer in every service that depends on it, commit that update in each repo, push it, watch each pipeline pick it up. The git history across XenTask services has dozens of “Updating Submodule” commits — not because the shared code changed often, but because keeping 13 repos in sync with one moving dependency is 13 times the overhead of keeping one repo in sync with itself.

The monorepo pattern solves this directly. Everything in one repo means shared code changes are atomic — one commit touches the library and the services that use it simultaneously. There’s no pointer to chase, no cascade of update commits, no risk of a service running on a stale version of a shared class because someone forgot to pull.

This is the observation that shaped UbixCore’s design. The monorepo isn’t an abstract preference — it’s the answer to a specific pain that only becomes visible once you’ve felt the submodule alternative at scale.

Use the Platform for Service Discovery

With 13 services, something has to know where everything is. The naive solution is a service registry — a central config that maps service names to addresses, updated when services move. The problem with a registry is that it’s another thing to maintain, another potential point of failure, and another layer of indirection.

XenTask’s API gateway does something simpler. When a request comes in for a resource that the gateway doesn’t handle locally, it constructs the service URL on the fly:

$micro_service = 'xentask-' . $request->RESOURCE;
$url = "http://" . $micro_service . ".micro-" . strtolower($_SERVER['DEPLOY_ENV']) . ".svc.cluster.local:8080";

The service name is the resource name. Kubernetes DNS handles the rest. xentask-forms is reachable at xentask-forms.micro-dev.svc.cluster.local. No registry. No config file. No service mesh. The naming convention is the discovery mechanism, and the platform provides the resolution.

The lesson here is broader than Kubernetes: before you build infrastructure to solve a problem, check whether the platform you’re already running on has already solved it. Kubernetes DNS-based service discovery is not a workaround — it’s the intended pattern, and it’s reliable enough to build on. The services that added complexity on top of it weren’t more robust; they were just harder to understand.

13 Pipelines Is 13 Places Things Can Break

Each XenTask service has its own CI/CD pipeline, its own Kubernetes deployment YAML, its own container build, and its own versioning. When a feature touches one service, that’s one pipeline to watch. When a feature touches three services — which happens regularly when you’re building something like task management, where the API, the WebSocket server, and the frontend all need to move together — that’s three pipelines, three potential failure points, and no atomic deployment.

The “Fixing API,” “Fixing pipeline,” and “Redeploy” commits scattered across the service histories tell the story. Not catastrophic failures — small, persistent friction. A pipeline timing out. An environment variable missing from one deployment but not another. A service that deployed successfully while the service it depends on was still mid-rollout.

This coordination overhead is not a reason to avoid microservices. It’s a cost you need to account for honestly. The right question before splitting a system into services is not “could these be separate?” but “do the operational benefits of separation — independent scaling, isolated failure domains, independent deployment cadence — outweigh the coordination cost of keeping them in sync?”

For XenTask, the WebSocket server in C is the clearest yes: it has a completely different runtime, different scaling characteristics, and different performance requirements than the PHP API layer. Separating it makes undeniable sense. For some of the other service splits, the answer is less obvious — and the submodule cascade and multi-pipeline coordination made that cost visible in a way that a diagram never would.

What It’s Worth Knowing Before You Start

Microservices are the right answer for some problems and the wrong answer for others. The cases where they’re clearly right: genuinely different runtimes that can’t coexist, components with wildly different scaling needs, or teams that need to deploy independently without stepping on each other.

The cases where the diagram looks clean but the reality is painful: shared code that belongs together, features that always touch multiple services simultaneously, and teams that are too small to absorb the operational overhead of maintaining 13 independent deployable units.

The architecture isn’t the mistake. Going in without a clear accounting of what independent services actually cost — in submodule churn, in pipeline coordination, in the discipline required to keep contracts between services stable — is the mistake. XenTask made the architecture work. It also made the costs visible in a way that informed every architectural decision that came after it.

Tags: Microservices Architecture Kubernetes DevOps XenTask