← Open in the full interactive course (progress tracking, search & more)

Lesson 249 taught the Dependency Rule in the abstract. This is what it actually looks like when it has to hold up under a real order-management API.

Lesson 249 gave you the Dependency Rule and a small, illustrative Product example: a domain interface, an application use-case, an infrastructure implementation, four rings on a diagram. That was enough to learn the rule. It was not enough to see what the rule costs and buys you across a codebase big enough to actually need it — four entities, five services, a real database, a real payment provider, and a background pipeline that has to keep working while all of that changes underneath it.

This lesson does not re-teach the Dependency Rule — go back to 249 if any part of "dependencies only point inward" feels shaky. What follows is OrderFlow's actual project structure: the real project names, the real interfaces, the real dependency graph, and the real trade-offs of applying that one rule to a system with genuine constraints, not a single illustrative class.

What Is It?

OrderFlow's solution is four projects, referencing each other in exactly one direction — inward, toward OrderFlow.Domain — with nothing else allowed to compile:

OrderFlow.sln ├── OrderFlow.Domain // Customer, Product, Order, OrderItem, and the interfaces they own ├── OrderFlow.Application // OrderService and the use-cases that orchestrate the domain ├── OrderFlow.Infrastructure // EF Core, the payment provider client, email — all the "how" └── OrderFlow.Api // ASP.NET Core endpoints, Program.cs — the composition root

The Dependency Rule's abstract "inner ring / outer ring" language from 249 becomes four concrete .csproj project references: Infrastructure and Api both reference Application and Domain; Application references only Domain; Domain references nothing of OrderFlow's own. No project reference ever points the other way — that's not a convention the team agreed to follow, it's a fact the build system enforces, exactly as 249's "Under the Hood" section described.

Why Does It Exist?

OrderFlow specifically needs this because two of its four real entities are, in the language of 249, genuinely swappable detail: the payment provider is a real external vendor OrderFlow doesn't control and might one day replace, and the database technology is a choice the team made, not a law of nature. If Order and OrderService were written against a specific payment SDK's types or EF Core's DbContext directly, replacing either one would mean touching the business logic itself — exactly the risk 249 identified as the core problem Clean Architecture exists to prevent. OrderFlow's scale and availability constraints from 333 make this sharper, not softer: a background pipeline (338) calling into PaymentService needs that call to go through an abstraction it can retry, mock in a test, and eventually swap, without the domain ever knowing which vendor is behind it.

Big Picture — What Lives in Each Project

ProjectContains
OrderFlow.DomainCustomer, Product, Order, OrderItem; IOrderRepository, ICustomerRepository, IProductRepository — owned here, per 249's rule that the abstraction belongs to the inside
OrderFlow.ApplicationOrderService (place/cancel an order); IPaymentGateway, IInventoryReserver, IShippingScheduler, INotificationSender — the abstractions the async pipeline (338) will depend on
OrderFlow.InfrastructureEfOrderRepository, EfProductRepository, EfCustomerRepository, OrderFlowDbContext (336); StripePaymentGateway, SqlInventoryReserver, EmailNotificationSender; the concrete PaymentService, InventoryService, ShippingService, NotificationService hosted processors (338, 339)
OrderFlow.ApiOrdersController, ProductsController; Program.cs — the one place every abstraction and its concrete implementation are wired together

How It Works — Placing an Order, Across the Four Projects

A REQUEST'S PATH THROUGH ORDERFLOW'S LAYERS
1. OrderFlow.Api RECEIVES THE REQUEST
2. OrderFlow.Application ORCHESTRATES
3. OrderFlow.Infrastructure IMPLEMENTS
4. OrderFlow.Api's Program.cs IS THE COMPOSITION ROOT

Simple Example — OrderService, End to End

// ═══ OrderFlow.Domain ═══ public class Order { public Guid Id { get; private set; } = Guid.NewGuid(); public Guid CustomerId { get; private set; } public OrderStatus Status { get; private set; } = OrderStatus.Placed; public List<OrderItem> Items { get; } = new(); public Order(Guid customerId, IEnumerable<OrderItem> items) { var itemList = items.ToList(); if (itemList.Count == 0) throw new ArgumentException("An order needs at least one item."); // a real domain rule CustomerId = customerId; Items.AddRange(itemList); } } public interface IOrderRepository { Task<Order?> GetByIdAsync(Guid id, CancellationToken ct); Task AddAsync(Order order, CancellationToken ct); } // ═══ OrderFlow.Application — references ONLY OrderFlow.Domain ═══ public class OrderService(IOrderRepository orders) { public async Task<Guid> PlaceOrderAsync(Guid customerId, List<OrderItem> items, CancellationToken ct) { var order = new Order(customerId, items); // domain rule enforced inside the constructor await orders.AddAsync(order, ct); return order.Id; } } // ═══ OrderFlow.Infrastructure — references Domain AND Application, plus EF Core ═══ public class EfOrderRepository(OrderFlowDbContext db) : IOrderRepository { public Task<Order?> GetByIdAsync(Guid id, CancellationToken ct) => db.Orders.Include(o => o.Items).FirstOrDefaultAsync(o => o.Id == id, ct); public async Task AddAsync(Order order, CancellationToken ct) => await db.Orders.AddAsync(order, ct); } // ═══ OrderFlow.Api/Program.cs — the composition root ═══ builder.Services.AddScoped<IOrderRepository, EfOrderRepository>(); builder.Services.AddScoped<OrderService>();

Meaning: Order.cs and OrderService.cs have no using Microsoft.EntityFrameworkCore; anywhere. You could delete OrderFlow.Infrastructure entirely and both files would still compile — the business rule "an order needs at least one item" survives untouched even if the database technology underneath it changes completely.

Real-World Example — Where the Payment Provider Actually Lives

The clearest proof this structure pays for itself: PaymentService depends on IPaymentGateway, an interface defined in OrderFlow.Application. Today, StripePaymentGateway implements it in OrderFlow.Infrastructure. If the business switches payment providers next year, here's the full blast radius:

ChangesUntouched
One new class in Infrastructure: NewProviderPaymentGateway : IPaymentGatewayOrder, OrderItem — the domain entities
One line in the composition root: which concrete type IPaymentGateway resolves toOrderService, and every other Application-layer use-case
PaymentService's own orchestration logic — it only ever called IPaymentGateway, never the vendor SDK directly

Nothing about that table is theoretical for OrderFlow specifically — it's the direct, practical payoff of the Domain Rule holding across a real vendor dependency, not just an interface swapped in a textbook example.

Analogy

The Recipe and the Kitchen

An order-placement rule — "an order needs at least one item" — is like a recipe: it says what has to be true of the finished dish, in terms that don't depend on which stove, which brand of oven, or which specific supplier's flour is being used. The kitchen equipment (Infrastructure) — a particular database engine, a particular payment vendor's SDK — is free to be replaced entirely, and the recipe doesn't need to be rewritten, because it was never written in terms of the equipment in the first place. OrderFlow's recipe book is OrderFlow.Domain and OrderFlow.Application; its kitchen is OrderFlow.Infrastructure.

Under the Hood — the Reference Graph, Made Concrete

249 explained that separate .csproj projects, with references only allowed inward, are what turn the Dependency Rule into a compiler error instead of a hopeful convention. Here's exactly what that means for OrderFlow's four .csproj files:

ORDERFLOW'S ALLOWED vs FORBIDDEN REFERENCES
ALLOWED
  • OrderFlow.InfrastructureOrderFlow.Application, OrderFlow.Domain
  • OrderFlow.ApiOrderFlow.Application, OrderFlow.Infrastructure (for DI registration only, not business logic)
  • OrderFlow.ApplicationOrderFlow.Domain
FORBIDDEN
  • OrderFlow.Domain → anything, including EF Core or the payment SDK
  • OrderFlow.ApplicationOrderFlow.Infrastructure directly
  • OrderFlow.DomainOrderFlow.Application

If a developer adds a NuGet reference to the Stripe SDK inside OrderFlow.Domain, the moment anything in that project actually tries to use a Stripe type, nothing stops the reference itself from being added — but the moment OrderService in Application tries to depend on a concrete Infrastructure type, the build fails outright, because Application has no project reference to Infrastructure at all. That's the enforcement mechanism working exactly as 249 described it: a missing project reference, not a code-review policy, is what makes the rule real.

Common Confusion

1. "OrderFlow.Api referencing OrderFlow.Infrastructure breaks the Dependency Rule" — not automatically

The Presentation layer (249's outermost ring) is allowed to reference Infrastructure for exactly one purpose: wiring concrete types to abstractions in the composition root, inside Program.cs. What it must never do is have a controller action call an Infrastructure type directly instead of going through OrderService or another Application-layer abstraction. The reference existing isn't the violation — using it for business logic instead of DI registration is.

2. "PaymentService, InventoryService, and the rest live in Application, since they're named like use-cases" — they actually live in Infrastructure

It's tempting to assume anything called a "service" belongs in the Application layer alongside OrderService. But PaymentService, InventoryService, ShippingService, and NotificationService (built out in 338 and 339) are hosted background processors that call real external systems — a payment vendor, an email provider — through concrete implementations of interfaces the Application layer defines. That makes them Infrastructure, by the same rule that puts EfOrderRepository there: they're the "how," not the "what."

Common Mistakes

Mistake 1 — Letting Order pick up an EF Core-friendly shape "just to make mapping easier"

Adding a public parameterless constructor and public setters to Order purely because EF Core's change tracker prefers that shape, instead of the private setters and constructor validation the domain rule actually calls for. Configure the EF Core mapping (in OrderFlow.Infrastructure, via Fluent API) to work with the entity's real, rule-enforcing shape — the entity's design is driven by the business rule, not by what's convenient for the ORM.

Mistake 2 — Having OrderService take IPaymentGateway's concrete Infrastructure type "just for this one call"

OrderService's constructor accepting StripePaymentGateway directly because the team "already knows" they're using Stripe and it felt faster than defining an interface. Every dependency OrderService takes must be an abstraction owned by OrderFlow.Application or OrderFlow.Domain — with zero exceptions, exactly the discipline 249 insisted on for the illustrative Product example, now applied to a dependency that's actually a paid, external vendor.

Mistake 3 — Splitting OrderFlow.Domain and OrderFlow.Application into two projects when the team doesn't yet need to

Reflexively creating both projects from day one because "that's how Clean Architecture diagrams show it," when OrderFlow's domain rules and its use-case orchestration are still small enough to genuinely live together without confusion. 249's own "When Should I Use It?" guidance still applies at this scale — OrderFlow's real complexity (five services, real external integrations, an async pipeline) is exactly the kind of complexity that earns the full four-project split; a much smaller system might reasonably combine Domain and Application into one project and still respect the Dependency Rule.

When Should I Use It?

Rule of thumb: If you can name a concrete Infrastructure dependency OrderFlow might genuinely need to swap or mock — the payment provider, the database, the email sender — that's your signal the Dependency Rule is earning its keep here, not just following a template.

Mental Model

OrderFlow.Domain = Customer, Product, Order, OrderItem, and the interfaces they own — references nothing.
OrderFlow.Application = OrderService and the abstractions PaymentService/InventoryService/etc. will need — references only Domain.
OrderFlow.Infrastructure = EF Core, the payment vendor, email — the concrete "how," referencing inward.
OrderFlow.Api = controllers plus the composition root — the only place every abstraction meets its concrete type.

Remember: the payoff isn't the folder structure itself — it's that swapping the payment provider or the database touches exactly one new class and one line in Program.cs.

Key Takeaway


Check Your Understanding

You've seen 249's Dependency Rule applied to OrderFlow's actual project structure. Let's confirm you can place a piece of OrderFlow code in the right project.

1. Which OrderFlow project should StripePaymentGateway, the concrete class that actually calls the Stripe SDK, live in?

Show answer

Correct: C

Why C is correct: StripePaymentGateway is concrete, vendor-specific detail — exactly the kind of "how" that belongs in Infrastructure, implementing an interface (IPaymentGateway) that Application owns and depends on.

Why A is incorrect: Domain holds core business rules and entities with zero outside dependencies — a concrete third-party SDK client is the opposite of that.

Why B is incorrect: Application holds the abstraction (IPaymentGateway) and the orchestration that depends on it — not the concrete vendor implementation itself.

Why D is incorrect: Being called eventually doesn't determine where a class lives — OrderFlow.Api holds controllers and the composition root, not vendor-specific business logic implementations.

Reinforcement: A concrete implementation of an abstraction the inside owns always belongs in Infrastructure, regardless of how "important" that dependency feels.

2. A developer adds a project reference from OrderFlow.Application to OrderFlow.Infrastructure so OrderService can call EfOrderRepository directly, skipping the IOrderRepository interface "just this once, for speed." What does this lesson say about that change?

Show answer

Correct: B

Why B is correct: The Under the Hood table lists "OrderFlow.ApplicationOrderFlow.Infrastructure directly" explicitly as forbidden — Application must depend only on abstractions it or Domain owns, exactly the discipline 249 required with zero exceptions.

Why A is incorrect: There's no special exception for repositories — the rule applies uniformly to every Infrastructure dependency, including data access.

Why C is incorrect: EF Core repositories are accessed through interfaces constantly — that's exactly what IOrderRepository and EfOrderRepository demonstrate working correctly.

Why D is incorrect: The Dependency Rule is a structural discipline about project references and testability, independent of whatever deployment topology OrderFlow eventually uses.

Reinforcement: "Just this once" is precisely how the Dependency Rule erodes — the rule holds with zero exceptions, or it stops meaningfully holding at all.

3. Why does this lesson classify PaymentService, InventoryService, ShippingService, and NotificationService as belonging in OrderFlow.Infrastructure, despite being named like Application-layer use-cases?

Show answer

Correct: B

Why B is correct: Common Confusion #2 makes this exact point — despite the use-case-sounding names, these four are concrete implementations calling real external systems, which is precisely the "how" that belongs in Infrastructure, behind interfaces Application owns.

Why A is incorrect: There's no such blanket C# naming convention — OrderService is also named "Service" and correctly lives in Application; the deciding factor is what the class actually does, not its name.

Why C is incorrect: They do use BackgroundService from lesson 165 (built out in 338) — that's unrelated to which project they belong in.

Why D is incorrect: Layer placement is determined by what a class depends on and whether it's concrete detail or abstraction/orchestration — not by whether it happens to run in the background.

Reinforcement: Layer placement follows what a class actually does (concrete detail vs. abstraction/orchestration), never its name or its execution model alone.

OrderFlow now has a real, enforced skeleton. Next: 335 decides exactly who's allowed to call which endpoint — applying JWT and policy-based authorization to OrderFlow's actual routes for the first time.


dotnetmadeeasy.com — Learn C# and .NET, the right way.