Your Microservices Justification Is Just Integration Convenience (And It's Costing You)

Your Microservices Justification Is Just Integration Convenience (And It’s Costing You)

When teams justify new services based on existing auth and client libraries instead of domain ownership, they’re building distributed monoliths. Here’s how to spot it and fix it.

There’s a moment in every architecture review where someone says the phrase that should trigger alarm bells: “We should build this as a service because we already have the integrations.”

What they mean is: the auth tokens work, the client library exists, someone else already did the hard part. The service will be easy to stand up.

What they’re actually saying is: we’re about to create a distributed monolith, and we’re going to call it microservices.

A recent Reddit thread from a fintech engineer at a ~10,000-person company captures this dynamic perfectly. Their team proposed creating a generic “user data” service so that any service wanting user data must route through it. When pressed on why, the answer wasn’t about domain ownership, business capability, or bounded contexts. It was: “our service already has the integrations like clients and s2s authentication enabled.”

That’s not architecture. That’s convenience masquerading as strategy.

The Integration Trap: Convenience Is Not an Architecture

Let’s be precise about what’s happening here. The team isn’t saying “we own the user domain and here’s the business value we provide.” They’re saying “we have plumbing, therefore we should become the choke point for everyone else’s data.”

 

This is backwards. It’s like a plumber arguing they should run the entire hospital because they installed the pipes.

The integration-ease argument creates a specific failure mode: services that exist not because they represent a coherent domain, but because they were the path of least resistance. The result is what one commenter on that thread aptly called a distributed monolith, a system where services are technically separate but functionally inseparable, deployed together, and coupled through a single shared schema.

The problems compound quickly:

  • No natural boundary: The service “keeps becoming bigger” because there’s no domain logic constraining its growth
  • API schema churn: Every small change requires coordinated updates across multiple services
  • Centralized coupling: A single s2s-auth-enabled service with everyone means every consumer depends on the same team’s roadmap

The most charitable interpretation, that a central service could apply common business logic transformations, fails on inspection. If the business logic is genuinely common, it belongs in a shared library, not a network call. If it’s not common, you’ve just created a god service that everyone routes through.

The “Generic User” Fallacy: There Is No Universal User

The deeper architectural smell here is the assumption that “user data” is a single, unified concept. It’s not. It’s a textbook case of bounded contexts being ignored.

Consider what “user” means across a real fintech platform:

Context What “User” Means
Payments Financial details, KYC status, transaction history
Fraud Detection Behavioral patterns, risk scores, device fingerprints
Account Management Personal info, authentication factors, preferences
Customer Support Interaction history, SLAs, open tickets
Compliance Regulatory data, audit trails, consent records
Different contexts require different user models with different constraints.

These are different models of the same entity, with different lifecycle requirements, different performance characteristics, and different security constraints. Fusing them into one generic /user_data endpoint with “all the fields” doesn’t just create coupling, it creates a single point of failure for every consumer.

 

As one responder on the thread put it, a user “looks different to a payment system (that needs to know financial details) and a delivery system (that needs to know address).” Forcing one service to serve both creates a schema that’s simultaneously over-broad and under-specific. Every team gets a schema chock-full of fields they don’t need, and none get the precise model they actually want.

This also creates an operational nightmare. A single generic endpoint with all user fields becomes a horsepower sink, every consumer pays the cost of serializing fields they’ll never read. And it’s not just performance. It’s security: a service with access to all user data becomes a honeypot, requiring infosec review for even tangential changes.

What Integration Ease Is Actually Buying You

Let’s steelman the “integration convenience” argument. There are cases where existing integrations genuinely matter:

Security posture

Already-vetted s2s auth reduces the surface area for new services

Operational familiarity

Teams comfortable with an established client library make fewer mistakes

Reduced provisioning overhead

Not needing to set up new auth flows saves real time

These are legitimate considerations. But they’re operational considerations, not architectural ones. They should inform how you build the service, not whether you build it.

The confusion between these two is where the hidden cost lives. Teams optimize for ease of standing up a service while ignoring the lifetime costs of maintaining it:

  • Every consumer coupling: Contract changes now require coordination with every downstream team
  • Cache invalidation complexity: A generic endpoint serving multiple contexts needs careful cache key design to avoid serving stale data across domains
  • On-call burden: When the generic service goes down, every consumer goes down with it

The real question is whether you’d still create this service if you had to build the integrations from scratch. If the answer is no, your integration convenience isn’t a justification, it’s inertia.

State Ownership, Not Integration Access

The core principle that resolves this debate is simple: architecture should follow state ownership, not integration convenience. Each service should own a distinct piece of state, and that ownership should map to a coherent business domain.

When you design around state ownership, integrations become a consequence of good design, not the cause of it. The auth setup follows the domain boundary, the domain boundary doesn’t contort itself to fit existing auth.

 

This is where the modular monolith becomes the pragmatic counterproposal. It gives you enforced boundaries and data ownership without the distributed systems tax. The trade-off matrix is stark:

Operational Vector Modular Monolith Microservices
p99 Internal Latency 1ms, 5ms (in-process) 25ms, 150ms (network)
Data Consistency ACID transactions Eventual consistency
Deployment Complexity Low (single artifact) High (orchestration)
Team Onboarding Fast (single repo) Slow (multiple repos)
A comparison showing the operational trade-offs between modular monoliths and microservices.

For most teams below ~50 engineers, the modular monolith is the smarter default. One article on the case for modular monoliths in 2026 cites 90% infrastructure cost reductions and CI runs collapsing from 45 minutes to under 5 in real-world migrations back.

The Boundary Test: Would You Build This If Everything Were Greenfield?

Before you let “we have the integrations” become a service justification, run these five questions:

1. Does the service own a full bounded context?

Can you define the service’s complete data model without referencing another service’s tables? If your best description is “it serves user data”, you don’t have a domain, you have a SELECT statement with deployment complexity.

2. Can you change the API independently?

If a consumer needs a field added, can your team deploy that change without coordinating with other teams’ release cycles? If the answer is “well, they need to update their clients to use new fields”, that’s not independence, that’s mandatory coordinated deployment wearing a trench coat.

3. Is the scaling profile genuinely different?

Does this service need to scale at a fundamentally different rate than its consumers? If everything in the company needs user data, the generic user service will have an identical load profile to every consumer. You haven’t decoupled scaling, you’ve created a bottleneck.

4. Can you fail independently?

When the generic user service goes down, does every consumer have a graceful degradation path? If the answer is “they all fail”, you’ve made the entire system’s availability dependent on the least stable component.

5. Does the data model serve a single context?

If your answer to “what is a user” varies by who’s asking, you need separate models. One project that adopted strict package boundaries with schema-per-module found that this eliminated entire classes of bugs around cross-module data corruption.

The Promotion Trap: When Headcount Drives Architecture

There’s a cynical but accurate observation on the original thread: the generic user service proposal was “promotion / retention driven development.” A full team to maintain the service means a manager with a platform, engineers with scope, and a line on a resume that says “led a company-wide initiative.”

This isn’t a trivial consideration. Architecture is a social activity, and individual incentives shape decisions. But it’s precisely when the social incentives and technical merits diverge that the hidden costs compound.

 

The team proposing the user data service isn’t necessarily malicious or incompetent, they’re responding to incentives. New services create headcount justification. Headcount justifies roles. Roles justify promotions. Meanwhile, the actual users of the system suffer through API churn and dependency hell.

Andrew Tanenbaum said, “We all like bad code, we’re just too embarrassed to admit it.” The same goes for architecture. The “generic user data service” is bad architecture, but it’s comfortable architecture, it lets people claim they’re doing microservices while avoiding the hard work of actually understanding their domain boundaries.

What the Right Approach Actually Looks Like

When you have a legitimate need for user data across services, the right answer isn’t a god service. It’s one of:

Option 1: Shared library for common semantics.

If the data transformations are genuinely generic (e.g., formatting, validation), put them in a versioned library. Consumers call the library, not a network endpoint. You get code reuse without network latency or availability coupling.

Option 2: Event-driven propagation of read models.

The owning service publishes domain events (user.updated, user.deleted). Consumers maintain their own read models from those events, keeping their schemas tailored to their domain. This is more upfront work, but it’s the pattern that scales to genuine independence.

Option 3: GraphQL federation if you genuinely need cross-cutting data access.

If the company already has graphQL routers and subgraphs (as the original poster’s company did), extend the existing setup to cover the new fields. One commenter made exactly this point: “this at least sounds like a decent candidate for a nice GraphQL implementation.”

Notice what none of these options do: create a new service with a generic endpoint and a full maintenance team. The microservices vs. monolith trade-off data consistently shows that the operational overhead of a new service, tracing, CI/CD, on-call rotations, observability, runs 5-10x the cost of the code itself.

When Microservices Actually Make Sense

To be clear: microservices aren’t wrong. They’re wrong for this justification. Legitimate signals for extraction include:

  • Sustained, measurable independent load on a specific module
  • Data-model divergence that schema-per-module can’t contain
  • 50 engineers organized into autonomous teams with separate ownership
  • Clear SLA, compliance, or fault-isolation requiring physical separation

One detailed analysis of when microservices pay off emphasizes the “genuinely” qualifier: “Don’t choose microservices because the architecture diagram looks more impressive. Choose them because independence solves a problem you actually have.”

Real-world examples reinforce this. Shopify and Discord’s architecture choices show companies at massive scale making deliberate choices away from indiscriminate microservices. The issue isn’t the pattern, it’s the reasoning used to justify it.

The Ask: Name the Domain, Not the Integration

Here’s the practical takeaway: when someone proposes a new service, force them to justify it in domain terms, not integration terms. If the justification is “we have auth and a client library”, that’s a smell. If it’s “we own the payments domain and here’s how that delivers business value”, that’s architecture.

This isn’t pedantry. The hidden costs of the integration-justified service aren’t in the first three months, they’re in the third year, when you’re coordinating cross-team API migrations for a service that was supposed to make things easier. The latency and complexity data shows that a five-service call chain routinely inflates p99 latency from 15ms to 300ms+. Every unnecessary service in that chain is a tax you pay on every request, forever.

The original poster’s instinct was right. The “user data” service, justified by existing integrations rather than domain ownership, is a distributed monolith in disguise. So is the payments service that exists because the payments team already had auth, and the notification service that exists because the infra team already had a Kafka cluster.

Here’s the case studies on what happens when teams reject microservices dogma. And here’s the proof that modular monoliths are eating microservices’ lunch.

The next time someone pitches a service based on integration convenience, ask them what domain it owns. If they can’t answer, you’re not building microservices, you’re building a distributed monolith with better marketing.

Share:

Related Articles