Clean Architecture Project in .NET: OrderFlow Capstone
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:
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
| Project | Contains |
|---|---|
| OrderFlow.Domain | Customer, Product, Order, OrderItem; IOrderRepository, ICustomerRepository, IProductRepository — owned here, per 249's rule that the abstraction belongs to the inside |
| OrderFlow.Application | OrderService (place/cancel an order); IPaymentGateway, IInventoryReserver, IShippingScheduler, INotificationSender — the abstractions the async pipeline (338) will depend on |
| OrderFlow.Infrastructure | EfOrderRepository, EfProductRepository, EfCustomerRepository, OrderFlowDbContext (336); StripePaymentGateway, SqlInventoryReserver, EmailNotificationSender; the concrete PaymentService, InventoryService, ShippingService, NotificationService hosted processors (338, 339) |
| OrderFlow.Api | OrdersController, 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
- OrdersController.PlaceOrder receives the authenticated request (335 covers exactly how) and calls into OrderService — it never touches EF Core or the payment provider directly.
- OrderService constructs the Order and OrderItem entities, applies domain rules (an order needs at least one item, quantities must be positive), and saves through IOrderRepository — an interface it depends on, never a concrete class.
- EfOrderRepository, living in Infrastructure, implements IOrderRepository using OrderFlowDbContext — this project references Domain and Application, never the other way around.
- builder.Services.AddScoped<IOrderRepository, EfOrderRepository>() is the one line in the entire solution that mentions both the abstraction and its concrete Infrastructure type together — exactly as 249 described, now with OrderFlow's real types in place of the illustrative Product example.
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:
| Changes | Untouched |
|---|---|
| One new class in Infrastructure: NewProviderPaymentGateway : IPaymentGateway | Order, OrderItem — the domain entities |
| One line in the composition root: which concrete type IPaymentGateway resolves to | OrderService, 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
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.Infrastructure → OrderFlow.Application, OrderFlow.Domain
- OrderFlow.Api → OrderFlow.Application, OrderFlow.Infrastructure (for DI registration only, not business logic)
- OrderFlow.Application → OrderFlow.Domain
- OrderFlow.Domain → anything, including EF Core or the payment SDK
- OrderFlow.Application → OrderFlow.Infrastructure directly
- OrderFlow.Domain → OrderFlow.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?
- OrderFlow's real, external payment dependency and its real database choice are exactly the kind of swappable detail 249 said earns the full four-project structure — this isn't ceremony for its own sake here.
- The four-project split pays for itself specifically because OrderFlow has a multi-person shape in mind and genuine complexity ahead (338's background pipeline, 339's messaging) — not because every application needs this by default.
- Revisit 249's own rule of thumb for any smaller project: internalize the underlying discipline (don't let business logic depend on framework detail) everywhere, but reserve the full multi-project ceremony for applications that will actually benefit from it.
Mental Model
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
- OrderFlow is four real projects — Domain, Application, Infrastructure, Api — with project references only ever pointing inward, exactly as 249's Dependency Rule requires.
- Order, Customer, Product, and OrderItem live in Domain with zero framework references; OrderService lives in Application, depending only on interfaces Domain and Application own.
- PaymentService, InventoryService, ShippingService, and NotificationService — despite being named like use-cases — are Infrastructure, because they're the concrete "how" behind interfaces Application owns.
- This structure is what lets OrderFlow swap its payment provider or its database technology by touching exactly one new Infrastructure class and one line in the composition root — a real, load-bearing payoff, not an academic exercise.
- Every remaining lesson in this Part builds inside this exact structure — 336's repositories live in Infrastructure implementing Domain-owned interfaces; 338's and 339's background services live in Infrastructure too, called through abstractions Application defines.
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.Application → OrderFlow.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.