The inner ring owns the interface

Pinch or Ctrl+scroll to zoom; the text is selectable. Source · TheYard

The inner ring owns the interface: Application declares IBidStore and uses it, EfBidStore and CosmosBidStore implement it from the outer ring, and the composition root decides which one each store gets The inner ring owns the interface Every green arrow crosses a ring and points inward, at IBidStore. Application compiles with neither adapter in it; only the composition root names both sides. Application: owns the interface api/TheYard.Application BidService BidService.cs, BidService.Bidding.cs Holds everybody's bids and asks BidRules before any is kept. Receives its store through the constructor and never learns which store that is: public BidService(IBidStore store) IBidStore, the port Ports.cs The shape of what Application needs from somewhere to keep bids, in Application's own words, with no SQL and no Cosmos DB in it. Task<IReadOnlyList<StoredBid>> LoadAsync(); Task SaveAsync(string userId, string vehicleId, BidState state); Task ClearAsync(string userId); NullBidStore Ports.cs The port wired to nothing: bids live in memory. The unit tests use it, and so does a store that has not come up yet. Outside: implements the interface api/TheYard.Infrastructure, .Infrastructure.Cosmos, .Api EfBidStore Infrastructure/EfSources.cs EF Core over Azure SQL Database or SQLite: one row per buyer per vehicle, replaced in place, three tries on a concurrency conflict. public sealed class EfBidStore(...) : IBidStore CosmosBidStore Infrastructure.Cosmos/CosmosSources.cs The Cosmos DB SDK: one document per buyer per vehicle, in the buyer's partition, so every write stays inside one partition. public sealed class CosmosBidStore(CosmosStore store) : IBidStore The composition root Api/Composition/StoreRegistration.cs The one place that names an adapter. It builds one backend per store at startup and hands each BidService its adapter: new BidService(new EfBidStore(contexts)) new BidService(new CosmosBidStore(store)) new BidService(NullBidStore.Instance) // until the store attaches implements implements calls hands in the adapter Held by OnionTests: Application_depends_only_on_Domain_Data_and_the_BCL, Application_never_reaches_the_disk_or_the_network, Services_are_registered_only_in_the_composition_root, Endpoints_never_reach_a_store_or_the_service_container. Source: docs/images/port-adapter.svg in the repository, drawn by docs/images/port-adapter.mjs and redrawn when the port or an adapter changes.