Distinguish source-code, build/binary, and runtime dependency direction. When a service calls another service over HTTP, in what sense can that dependency still be 'inverted', and who should own the contract?
answer
- source ≠ build ≠ runtime arrows
- DIP reverses knowledge, never the call
- plugin host: runtime-only dependency
- SDK import = conformist; ACL = translate
- events invert the runtime arrow, cost consistency
basics
~20 sSource dependency = whose code names whose; build dependency = which artifact must be present to compile/link; runtime dependency = who calls whom and must be running. Over HTTP the caller still needs the callee at runtime, but you can invert the source/contract direction by having the consumer define the interface it needs and the provider satisfy it.
solid answer
~60 sThree separable directions: **source** (module A names a symbol in B, so A cannot compile without B), **build/binary** (artifact A links or packages artifact B; can exist without a source dependency, e.g. a plugin loaded reflectively), and **runtime** (A calls B and needs B alive; observable as latency, availability and deploy coupling). DIP removes source dependencies but never removes the runtime one — the policy still calls the database. Across services, the same split applies: the caller has a hard runtime dependency, but the *contract* direction is a design choice. If the provider publishes a client SDK/schema that the consumer imports, the source/knowledge arrow points consumer → provider. If instead the consumer declares the interface it needs (consumer-driven contracts, an anti-corruption layer, or a provider-published schema the consumer maps into its own model), the consumer stays independent of provider vocabulary and provider changes get validated against consumer expectations in CI. You can also invert the *runtime* arrow with events: instead of the core calling out, the provider publishes and consumers subscribe, so the emitter knows nothing about the receivers. Trade-offs: consumer-owned contracts scale badly past a handful of consumers; provider-owned schemas centralize evolution but leak vocabulary.
go deeper
Distinguish 'my code mentions yours' from 'my code calls yours while running', and note that an interface removes the first but not the second.
Add the build/binary dimension with the plugin example, and explain that a generated HTTP client belongs in an outer adapter, not in the core.
Discuss contract ownership options — provider SDK, published schema plus anti-corruption layer, consumer-driven contracts — with their trade-offs, and the deploy-order symptom of runtime cycles.
Tie boundary vocabulary to DDD context-mapping choices, weigh contract-testing scalability against schema governance, and treat event-driven inversion as a deliberate consistency trade rather than a default.
## Three dependency directions, defined | Kind | Meaning | Broken by | Symptom when wrong | |---|---|---|---| | **Source** | Module A mentions a name declared in B; A's code cannot compile/parse without B's declarations | Introducing an abstraction owned by A that B implements (DIP) | Editing B forces edits/recompiles in A; A untestable alone | | **Build / binary** | Artifact A must have artifact B on its classpath/link line, or packages it | Loading implementations dynamically (plugins, service loaders, DI at startup), separate artifacts | Slow builds, fat artifacts, version conflicts (diamond dependencies) | | **Runtime** | A invokes B during execution and requires B to exist/respond | Asynchrony, events, queues, caching, fallbacks — or nothing, if the work genuinely must be done | Cascading failure, latency coupling, ordered deploys | They are independent. Examples that make it click: - **Source, not runtime**: A imports a constant or a type from B, and at runtime never calls it. - **Runtime, not source**: A gets an `OrderStore` injected and never names any implementation. This is what DIP produces. - **Build, not source**: the deployable bundles a driver that only the DI configuration references by string/config. **Key insight**: DIP and the Dependency Rule reverse *source* (and often *build*) arrows. They do **not** reverse runtime arrows. A use case that needs a database still needs a database. Claims like "hexagonal architecture removes the database dependency" are wrong; it removes the *knowledge* dependency. ## The plugin case (source vs. build, made concrete) A plugin host defines an interface; plugins implement it; the host discovers them via a service loader, a directory scan, or configuration. The host has **no source and no build dependency** on any plugin, yet at runtime it calls them. This is the purest expression of dependency direction control — and the shape Clean Architecture aims for with the framework/DB. ## Across a network boundary When A calls B over HTTP/gRPC, A has an unavoidable **runtime** dependency: availability, latency, and failure modes propagate. What remains a *choice* is the **contract/knowledge** direction: 1. **Provider-owned client library.** B publishes an SDK; A imports it. Fast to adopt; but A now has a source + build dependency on B's vocabulary and release cadence, and B's model leaks into A's code. Upgrades of the SDK become coordination events. 2. **Provider-owned schema, consumer-owned model.** B publishes OpenAPI/protobuf/AsyncAPI; A generates or hand-writes a thin client and immediately maps into *its own* domain types behind an **anti-corruption layer** (a translation boundary that keeps a foreign model out of your core). A's core depends only on A's interface; the generated client is an outer-ring adapter. This is usually the best default. 3. **Consumer-driven contracts.** A publishes the expectations it has of B (via a contract-testing tool); B verifies them in its own CI. The *direction of definition* flows consumer → provider, so B cannot silently break A. Excellent with a handful of known consumers; unmanageable with dozens or public/unknown consumers, where a provider-published, versioned, backward-compatible schema wins. 4. **Invert the runtime call with events.** Rather than A calling B, B (or A) publishes a domain event to a broker and interested parties subscribe. The emitter has no source, build, or runtime knowledge of subscribers — arrows point from subscribers to the event schema. Cost: eventual consistency, ordering/duplication concerns, harder debugging, and the event schema itself becoming a stable, widely depended-upon contract that must be versioned carefully. ## Ownership heuristic Ask *whose vocabulary should win at the boundary*. In Domain-Driven Design terms: with a **Customer/Supplier** relationship the downstream can demand a contract (consumer-driven); with **Conformist** the downstream simply accepts upstream's model; with an **Anti-Corruption Layer** the downstream accepts nothing and translates. Choosing consciously is the architectural act; drifting into Conformist by importing the vendor SDK into your core is the common accident. ## Distributed cycles Mutual runtime calls (A → B → A) reproduce the Acyclic Dependencies Principle failure at deployment scale: neither service can be released or rolled back independently, and a fault in one can feed back through the other. Break them the same two ways — invert one direction (callback → event, or move the decision to the caller) or extract the shared concept into a third service or a shared, versioned contract. ## Practical checks - Grep your core module for vendor package names: any hit is a source-direction violation. - Look at your CI: can the core artifact build with the network unplugged and no framework on the classpath? If not, the source/build arrows are still pointing outward. - Look at your deploy runbook: if it prescribes an order ("deploy B before A"), you have a runtime dependency cycle or an unversioned contract.
- Does hexagonal architecture remove a service's dependency on its database?No — only the source/build dependency. The runtime dependency remains: the use case still needs a live database. What you gain is the ability to compile, test, and reason about policy without the database, and to swap the adapter.
- When are consumer-driven contracts the wrong choice?When the provider has many or unknown consumers (public APIs, platform services). Verifying every consumer's expectations becomes a bottleneck; a provider-published, versioned, backward-compatible schema with deprecation windows scales better.
- How do events change the dependency direction between two services?The emitter publishes to a broker and names no subscriber, so source, build and runtime knowledge all point from subscribers toward the event schema. The trade is eventual consistency, ordering/duplication handling, and a shared schema that itself needs careful versioning.
saying these in an interview costs you the question
- Claiming ports and adapters remove the runtime dependency on the database or third-party API.
- Importing a provider's client SDK types directly into business logic and calling it decoupled.
- Assuming a source dependency and a runtime dependency always point the same way.
- Treating consumer-driven contracts as universally superior regardless of consumer count.
- Using events purely to 'decouple' without accounting for eventual consistency, ordering, and duplicates.
- Solving a mutual service dependency by sharing one database schema between the two services.
- Ignoring deploy-order requirements as an ops detail rather than a symptom of a runtime cycle.