Microservice Dependencies: How @requires Makes Them Explicit

cover
Jens Neuse

Jens Neuse

min read

TL;DR

Most microservice failures aren't an architecture problem β€” they're a visibility problem. This post maps several types of microservice coupling and shows how GraphQL Federation's @requires directive moves dependencies from hidden application code into the schema, where they're validated at build time and visible in query plans.

If two or more microservices share the same database, you don't have microservices. You have a distributed monolith.

That's not a controversial opinion anymore.

Twilio Segment famously walked away from 140 microservices managed by 3 engineers. Many teams have publicly documented reverting from microservices to monoliths for small-to-medium applications.

The pendulum is swinging back, and the backlash is loud. But here's the thing: the problem was never microservices themselves. The problem was implicit dependencies.

Most teams that "failed at microservices" failed because they moved from a monolith where all dependencies were visible to a distributed system where the dependencies became invisible. Hidden in HTTP calls buried in application code, shared libraries that create cascading update requirements, or databases that multiple services quietly read from.

The goal of microservices is team autonomy. But autonomy requires independence, and independence requires that your dependencies are explicit. If you can't see your dependencies, you can't manage them.

In this post, I'll introduce a taxonomy of microservice dependencies, explain why most architectures only address one or two types while ignoring the rest, and show how GraphQL Federation's @requires directive makes all of them explicit.

Introducing WunderGraph Hub: Rethinking How Teams Build APIs

WunderGraph Hub is our new collaborative platform for designing, evolving, and shipping APIs together. It’s a design-first workspace that brings schema design, mocks, and workflows into one place.

The Taxonomy of Microservice Dependencies

When engineers talk about microservice dependencies, they usually mean "Service A calls Service B." But that's a massive oversimplification.

Sam Newman, in his book Building Microservices , identifies four types of coupling. The broader industry adds several more. Here's the full taxonomy:

1. Domain Coupling

Service A interacts with Service B because of what B does. An Order service needs a Payment service to charge a card. This is unavoidable because services exist to do things for each other.

But is it visible?

2. Data Coupling

Service A needs specific data from Service B to compute something. A Shipping service needs the product weight from the Product service to calculate shipping costs. A Pricing service needs the user's subscription tier from the User service to apply discounts.

This is the coupling type most people think about first, and it's widely considered the hardest to solve. Christian Posta, VP at Solo.io and author of Istio in Action, argues that the hardest part about microservices is your data.

3. Temporal Coupling

Service A needs Service B to be available right now. This happens whenever services communicate synchronously. If the Product service is down, the Shipping service can't calculate costs, even though all it needed was a weight field.

Temporal coupling is the #2 most discussed problem after data coupling, and it causes more production outages than any other type.

4. Pass-Through Coupling

Service A sends data through Service B to reach Service C. The Order service sends a shipping manifest through the Warehouse service to the Shipping service. Service B doesn't need the data, it just relays it.

This creates hidden dependencies. If the Shipping service changes its manifest format, both Service A and Service B need to change, even though Service B never uses the data.

5. Common Coupling

Multiple services share the same resource. A shared database, file system, or cache.

This is the classic "distributed monolith" smell. If two services read from the same database table, they are coupled through that table's schema. Change the schema, and both services break.

6. Contract Coupling

Service A depends on Service B's specific API interface. REST endpoint paths, request/response shapes, protobuf schemas. When B changes its API, A breaks.

This is well-understood, and tools like Pact and semantic versioning exist to manage it. But discipline is rare.

7. Organizational Coupling

Team boundaries don't match service boundaries. Team A owns a service but needs Team B to change another service first.

Martin Fowler and Sam Newman both argue this is often the root cause that manifests as all other types of coupling. Conway's Law isn't just an observation β€” it's a force of nature.


The Real Problem: Implicit vs. Explicit

Here's my central argument: every type of coupling above becomes manageable when it's explicit, and becomes toxic when it's implicit.

Let's look at what "implicit" looks like in practice.

In a traditional microservice architecture, Service A needs the product weight to calculate shipping costs. So a developer writes an HTTP call:

1
2
3
4
5
6
7

This single piece of code creates three types of coupling at once:

  1. Data coupling β€” we depend on the weight field from the Product service
  2. Temporal coupling β€” the Product service must be up right now
  3. Contract coupling β€” we depend on the exact REST endpoint path and response shape

We’ve also hardcoded the service address, turning a logical dependency into a deployment constraint.

And none of it is visible outside this function. No architect looking at the system can see this dependency without reading every line of code in every service. No build system validates whether the Product service actually has a weight field. No query planner can optimize the call pattern.

Now imagine 50 services, each with dozens of these hidden calls.

That's not a microservice architecture. That's a distributed monolith with extra latency.


So What Can We Do About It?

We've established that the real problem is that most dependencies are implicit. Hidden in code, invisible at design time, and only discovered when something breaks.

What we need is an architecture that makes dependencies explicit by default. Not through discipline or documentation, but through the system itself.

That's exactly what GraphQL Federation does, or more specifically, the @requires directive.

A Brief Primer on GraphQL Federation

If you're already familiar with federation, feel free to skip ahead.

GraphQL Federation is a way to distribute a single GraphQL API across multiple services. Each service, called a subgraph, owns a portion of the overall schema. A router sits in front of all subgraphs, composes their schemas into a single unified API, and orchestrates requests across them.

The key building block is the entity. An entity is a type that can be uniquely identified and resolved across multiple subgraphs. You declare an entity by adding a @key directive:

1
2
3
4
5

The @key tells the router: "You can look up a Product by its id from this subgraph."

Multiple subgraphs can contribute fields to the same entity. The router uses the key to stitch the data together. From the client's perspective, it looks like a single API.

This is what makes federation different from API gateways or schema stitching. The services don't know about each other. The router handles all the coordination, and the schema enforces consistency through composition checks at build time.

For a deeper dive, see The Evolution of GraphQL Federation and the Entity Layer .


How @requires Makes Dependencies Explicit

With federation, services don't call each other. The router orchestrates all data fetching, and dependencies are declared in the schema using the @requires directive.

Here's the same shipping cost example:

1
2
3
4
5
6
7
1
2
3
4
5
6

That's it. The Shipping subgraph declares: "To resolve shippingCost, I need the weight field, which is owned by another subgraph."

The federation router handles the rest. When a client queries shippingCost, the router knows it must first fetch weight from the Product subgraph, then pass it to the Shipping subgraph's resolver.

Let's look at what changes:

Coupling TypeREST (Implicit)Federation @requires (Explicit)
DomainService A depends on Service B’s capabilitiesDependency is expressed through shared entities and fields in the schema
DataHidden in application codeDeclared in the schema: @requires(fields: "weight")
TemporalShipping service must call Product service directlyServices don't call each other; the router still needs both subgraphs available, but the dependency is visible in query plans
Pass-throughService B relays data it doesn't needRouter handles multi-hop resolution
CommonTemptation to share a databaseEach subgraph owns its data; @requires bridges the gap
ContractDepends on REST endpoint paths and response shapesDepends on specific GraphQL fields, validated at composition time
OrganizationalHidden dependencies require back-channel coordinationSchema makes dependencies visible to all teams

To be clear: @requires doesn't make temporal coupling disappear. The router still needs the Product subgraph to be available to fetch the weight field. But the dependency is no longer hidden in a function body somewhere. It's in the schema, the query plan, and it's validated at build time.

The dependency still exists, but now you can see it, plan for it, and manage it.

The Power of Explicit: What You Can See, You Can Manage

Once dependencies are in the schema, you unlock capabilities that are simply impossible with implicit dependencies.

Dependency Graphs at Design Time

With @requires, every cross-service dependency is a first-class citizen of the schema. You can compute a full dependency graph without running a single request. Which subgraph depends on which fields? Which fields are the most "depended upon" across the entire federated graph?

If all your subgraphs depend on a single service through @requires, you can see that immediately. That service is a bottleneck, and everyone should pay extra attention to its reliability.

With implicit HTTP calls, you'd need to analyze runtime traces, logs, and metrics to get the same picture. Even then, you'd only see the dependencies that actually fired during your observation window.

Query Plans Show Exactly What Happens

Every query in a federated graph produces a query plan. The query plan shows exactly which subgraphs will be called, in what order, and which fields are fetched at each step.

When you add a @requires dependency to a field, you can immediately see how query plans change. You can preview the performance impact before deploying anything.

Compare this to adding a new HTTP call in a REST service. How do you know the impact? You have to deploy it, watch the metrics, and hope nothing breaks. With explicit field dependencies, you see all of this at design time.

Composition Checks Validate Dependencies at Build Time

When you publish a subgraph schema that uses @requires, the composition engine validates that the required fields actually exist, that they're correctly marked as @external, and that the dependency graph is resolvable.

If someone removes a field that another subgraph requires, the build fails. Not the runtime. Not a production incident. The build.

Compare this to REST APIs, where you might deprecate an endpoint or change a response shape, and the only way to know if something breaks is to deploy and find out. With federation, you simply can't break the contract. Composition checks enforce it.

This is the equivalent of a compiler catching a type error. Except instead of a single codebase, you're validating contracts across an entire distributed system.

With tools like WunderGraph Hub , you can see these dependencies at design time, preview how query plans change when you add or modify a @requires dependency, and catch breaking changes before they ever reach production. The dependency graph isn't just explicit, it's actively guarded.


Field-Level Precision: Not Endpoints, Fields

There's a subtlety in @requires that's easy to overlook but incredibly important.

With REST APIs, you depend on entire endpoints. Your service calls GET /products/{id} and gets back the full product object, even if you only need the weight. The Product service has no idea which fields you actually use.

With @requires(fields: "weight"), the dependency is at the field level. The Product subgraph knows exactly which fields other subgraphs depend on. Not which endpoints. Not which objects. Which specific fields.

This precision has real consequences:

  • The Product team knows that weight is a critical field because 3 other subgraphs require it. They'll think twice before changing its type or removing it.
  • The Shipping team knows they depend on exactly one field from one subgraph, not an opaque REST endpoint that might change.
  • The platform team can identify the "hotspot" fields that serve as edges between services and monitor them accordingly.

If we combine the fields most used as "edges" between two services, we know exactly where the bottlenecks and coupling hotspots are. This is information that simply doesn't exist in an implicit architecture.


A Note on @provides

I want to be transparent about @provides because I see it discussed as a companion to @requires, but I think the comparison is misleading.

@provides declares that a resolver can return fields that are owned by another subgraph. It's an optimization hint for the router: "When you call this field, I can also give you these other fields cheaply, so you don't need to make another fetch."

1
2
3
4
5
6
7
8
9

This can be useful in specific cases, but it comes with a tradeoff that's rarely discussed: @provides makes fields shareable, meaning they can be resolved from multiple subgraphs.

In many cases, this is an anti-pattern. If two subgraphs can resolve the same field with the same data, the data needs to live somewhere accessible to both. And if the data is the same, @provides might be an indication that the data source is shared.

That's not always the case, but it's a smell worth investigating.

My recommendation: use @requires for expressing dependencies. Use @provides sparingly, and only when you're certain the data isn't shared from a common source.


Service Boundaries: The Root Cause Most Teams Miss

Before reaching for any technical solution, there's a more fundamental question most teams get wrong: where do you draw the service boundary?

The most common mistake I see is creating too many services. Teams split along technical layers (a database service, an auth service, a notification service) instead of along team boundaries.

My rule is simple: a team should be able to ship a product change by modifying a single service.

If a product change requires coordinated changes across multiple services, your services are too small. Code that changes together should live in the same subgraph.

If multiple teams must coordinate to make a single product change, your service ownership is spread too wide.

The goal is team autonomy. Autonomy brings velocity. A good starting point is one service per team, although a team can own multiple services when they are truly independent β€” for example, when the code never changes together in the same PR.

Once your boundaries are right, federation's @requires makes the remaining inter-service dependencies explicit, manageable, and visible. But no amount of tooling will fix bad boundaries.


The Industry Is Stuck on Implicit

Let me tie this back to industry trends we see.

Companies are reverting to monoliths, not because microservices are fundamentally flawed, but because the tooling for managing implicit dependencies never materialized.

The alternatives people reach for:

  • Event-driven choreography β€” reduces temporal coupling but introduces eventual consistency, event ordering complexity, and makes the dependency graph even more implicit
  • API gateways β€” manage contract coupling but do nothing for data, temporal, or organizational coupling
  • Contract testing tools like Pact β€” useful but low adoption, and they only catch contract coupling
  • "Just be more disciplined" β€” not actionable at scale

The gap is clear: There is no widely adopted solution that makes all types of microservice dependencies explicit and manageable at design time.

Except for federation with @requires.

The schema becomes the single source of truth for your dependency graph. Composition checks validate it at build time. Query plans make it visible at design time. Field-level precision tells each team exactly who depends on what.

This isn't a new problem. But it's a problem that finally has an elegant solution.


Conclusion

Microservice dependencies are not a single problem. They're several problems: domain, data, temporal, pass-through, common, contract, and organizational coupling.

Most architectures address one or two. The rest stay implicit, hidden in application code, and slowly turn your microservices into a distributed monolith.

GraphQL Federation's @requires directive addresses most of them simultaneously through one mechanism: making dependencies explicit and moving orchestration to the router.

What you can see, you can manage. What you can't see will eventually page you at 3 AM.

Make your dependencies explicit. Your future self will thank you.

If you want to see what explicit dependency management looks like in practice β€” dependency graphs, query plan previews, and composition checks that catch breaking changes before they reach production β€” take a look at WunderGraph Hub .


Frequently Asked Questions (FAQ)

The industry identifies several types: domain coupling, data coupling, temporal coupling, pass-through coupling, common coupling, contract coupling, and organizational coupling. Most architectures only address one or two of these, leaving the rest as implicit, invisible dependencies.

@requires declares that a field in one subgraph depends on specific fields from another subgraph. The federation router fetches the required data before calling the resolver. This makes cross-service data dependencies explicit in the schema, visible in query plans, and validated at composition time.

With HTTP calls, the dependency is hidden in application code, the calling service must know the other service's address, and both services must be available simultaneously. With @requires, the dependency is declared in the schema and the router handles orchestration. The temporal dependency still exists β€” the router still needs the subgraphs to be available β€” but the dependency is explicit and visible in query plans rather than hidden in code.

Implicit dependencies are hidden in application code β€” synchronous HTTP calls, shared libraries, or direct database access. They are invisible at design time and only surface during runtime failures. Explicit dependencies are declared in the schema, validated at build time, and visible in query plans before a single request is made.

@provides is an optimization that declares a resolver can return additional fields owned by another subgraph. While useful in specific cases, it makes fields shareable across subgraphs, which can be an anti-pattern if it indicates shared data sources. Use it carefully and prefer @requires for expressing dependencies.

Services should be split so that a team can autonomously ship product changes without coordinating across multiple services. Code that changes together should live in the same service. A good starting point is one service per team. If you regularly need to change multiple services for a single product change, your services are too small.


Jens Neuse

Jens Neuse

CEO & Co-Founder at WunderGraph

Jens Neuse is the CEO and one of the co-founders of WunderGraph, where he builds scalable API infrastructure with a focus on federation and AI-native workflows. Formerly an engineer at Tyk Technologies, he created graphql-go-tools, now widely used in the open source community. Jens designed the original WunderGraph SDK and led its evolution into Cosmo, an open-source federation platform adopted by global enterprises. He writes about systems design, organizational structure, and how Conway's Law shapes API architecture.