Featured resource
2026 Tech Forecast
2026 Tech Forecast

1,500+ tech insiders, business leaders, and Pluralsight Authors share their predictions on what’s shifting fastest and how to stay ahead.

Download the forecast
  • Lab
    • Libraries: If you want this lab, consider one of these libraries.
    • Core Tech
Labs

Details Crowd the Core in Clean Architecture

ShelfSync is the restocking backend for a small chain of hardware stores. Store managers report low stock and place reorders through it; a nightly job reads a low-stock report to plan truck routes. The existing code has StockItem objects that carry EF Core annotations and JSON-shaping properties alongside the actual business rule (when does an item need reordering, and by how much). You will extract that rule into a clean domain entity, route all reordering through an application use case that only knows about repository and notifier interfaces, split the read-heavy low-stock report away from the write path, and wire concrete EF Core and notification adapters in from a composition root - keeping one simple lookup endpoint deliberately un-layered because the abstraction would not pay for itself there.

Lab platform
Lab Info
Level
Beginner
Last updated
Nov 02, 2026
Duration
45m

Contact sales

By clicking submit, you agree to our Privacy Policy and Terms of Use, and consent to receive marketing emails from Pluralsight.
Table of Contents
  1. Challenge

    Step 1: Meet ShelfSync's tangled restocking core

    ShelfSync is the restocking backend behind a small chain of hardware stores. Store managers log low stock and place reorders through it, and a nightly job reads a low-stock report to plan the next day's delivery routes. Both jobs sound simple, but the code that backs them has grown a single StockItem class that tries to be a database row, a JSON payload, and a business rule all at once. Picture that class: [Table("stock_items")] and [Column("qty")] attributes sit next to a [JsonPropertyName("currentQty")] property, and buried between them is the one line that actually matters — the comparison that decides whether an item needs reordering. Change how the table is named, and you risk touching the same file that decides whether a truck gets dispatched. That coupling is the whole problem this lab addresses. The starter has already moved the annotations out: StockItemRow in src/Infrastructure/ShelfSyncDbContext.cs now holds the [Key] and [Column] attributes, and StockItem in src/Domain/StockItem.cs is left with only the fields the reorder rule needs. Clean Architecture answers this by drawing a line around the business rules — the Domain layer — and enforcing the Dependency Rule: outer layers may depend inward on the domain, but the domain never depends outward on a database, a web framework, or a UI. An Application layer sits just outside the domain and orchestrates use cases purely through interfaces — ports — that name what the domain needs (a repository to load and save, a notifier to alert) without saying which concrete technology answers them. Concrete Infrastructure adapters — EF Core, an email sender — implement those ports from the outside, and a composition root is the one place allowed to wire a concrete adapter to the interface it satisfies. Over the next four steps you will pull StockItem's reorder rule into a clean Domain entity, build Application use cases — ReorderService and LowStockAlertService — that only ever see IStockRepository and INotifier, split the low-stock report into its own read-optimized projection so it never has to replay domain rules, and finish by wiring EF Core adapters in from a composition root. One endpoint — a single-item quantity lookup — will deliberately skip all of this, because a one-field read has no rule to protect and no write to coordinate; you'll build it directly against the database and see why that's the right call, not a shortcut.

    info> If you get stuck, you can refer to the provided solution code for each task, available in the solution folder.

  2. Challenge

    Step 2: Give StockItem a business-only vocabulary

    Every StockItem currently answers 'does this need reordering?' with logic scattered wherever someone last needed the answer. src/Domain/StockItem.cs holds the entity you'll fix that in: it already has the fields — Quantity, ReorderThreshold, ReorderQuantity — but its methods don't yet use them. An anemic entity like this is just a data bag; a rich one answers questions about itself. You'll turn StockItem into the latter, one method at a time, without it ever importing anything about databases or HTTP. With NeedsReorder() telling callers when to act, the entity should also tell them how much to act by — otherwise every caller ends up re-deriving that arithmetic from Quantity and ReorderQuantity themselves. The last gap is the other direction: something has to be the single place Quantity is allowed to increase, so that a badly-formed delivery can never leave the entity in a state NeedsReorder() was never designed to see.

  3. Challenge

    Step 3: Orchestrate reordering through injected ports

    With the domain rules in place, src/Application/ReorderService.cs is where reordering actually happens — but this file must never construct a database connection or an HTTP client. It depends only on IStockRepository and INotifier, two ports already declared in src/Application/IStockRepository.cs and src/Application/INotifier.cs, and on the StockItem methods you just wrote. That's the Dependency Rule in practice: the use case names what it needs and lets something else decide how those needs get met. Placing a reorder is only half the workflow — the pending amount it records has to be cleared once the delivery actually lands, or the item will look like it's still waiting on stock that already arrived. PlaceReorderAsync only ever asks about the one item a caller names. The nightly job needs the opposite: a sweep across every item in the warehouse, which is a separate use case with its own file, src/Application/LowStockAlertService.cs.

  4. Challenge

    Step 4: Split the command path from the low-stock report

    So far, reading whether an item needs reordering and writing a stock change have shared the same path through the domain entity. That's fine for one item, but a report that scans every item in the warehouse and re-runs NeedsReorder() for each one is answering a question the write side already answered when the change was committed. CQRS separates the two: a command handler owns writes, and a read-optimized projection — a denormalized snapshot kept in sync by the command side — answers queries without touching the domain at all. In this lab the command side covers stock adjustments. ReorderService from Step 3 still saves deliveries on its own, so a fuller split would route those through the handler too, or the report would fall behind. You'll build the command path first, in src/Application/StockCommandHandler.cs. The command handler now calls out to a projection store after every write — that store is src/Infrastructure/LowStockProjectionStore.cs, and it's the piece responsible for actually keeping the snapshot honest as items cross in and out of needing reorder. With the snapshot kept current on every write, the report itself gets simple: src/Application/LowStockReportService.cs should never have to load a StockItem or call NeedsReorder() again — it just reads what the projection already knows.

  5. Challenge

    Step 5: Wire adapters in and judge where the layering pays off

    Every piece so far has depended on interfaces — IStockRepository, INotifier — without ever being told which concrete class answers them. Something has to make that decision, and Clean Architecture puts it in exactly one place: a composition root. Before you can write that root, though, you need a real adapter for it to wire in. src/Infrastructure/EfStockRepository.cs is where IStockRepository finally meets an actual EF Core DbContext. With a concrete repository (and the notifier the starter already provides) in hand, src/CompositionRoot.cs can assemble a fully wired ReorderService — the only file in the solution that's allowed to know about both the interfaces and the classes that implement them. The other use cases are already wired there for you, so BuildReorderService is the one method left to write. Not every read deserves this treatment. src/Api/StockLookupEndpoint.cs exposes a single-item quantity lookup with no business rule behind it and nothing to coordinate — routing it through a repository, a use case, and a composition-root wire-up would be ceremony with no payoff. Build it directly against the database and let the contrast make the case for everything before it.

  6. Challenge

    Step 6: Watch the clean core run ShelfSync

    Every task is done and graded. Now run ShelfSync as one program. src/Program.cs seeds four items into an EF Core in-memory database, then drives a reorder, the nightly sweep, and the low-stock report through the code you wrote. You'll finish by changing one line in the domain and watching both sides of the CQRS split follow it. ### Follow one reorder through every layer

    In the Terminal tab, run:

    dotnet run --project src/Lab.csproj
    

    Look at the -- Reorder, wired by CompositionRoot -- section. Reorder placed for HAM-16: 26 units passed through every layer you built. Your BuildReorderService from Task 5.2 wired the EF adapter into ReorderService, GetByIdAsync from Task 5.1 turned the database row into a domain StockItem, and the entity's own NeedsReorder() and CalculateReorderQuantity() worked out 30 - 4 = 26. TAP-25 has 22 on the shelf against a threshold of 8, so it gets 0 units.

    The lookup lines read the same database without any layers. HAM-16 has 4 on the shelf becomes HAM-16 has 30 on the shelf once ConfirmReceiptAsync saves the delivery, and an unknown item gives NAIL-99 returns -1. That's your one-method GetQuantity from Task 5.3. It has no repository and no use case, and it still sees exactly what the layered path wrote.

    The last line starts with Delivery of 0 for DRL-18 rejected: and ends with your exception's message. That's the guard in ReceiveStock from Task 2.3. ReorderService never checks the amount itself. The rule lives in the entity, so every caller gets it. ### Compare the sweep with the report

    The next two sections answer the same question in two different ways. The nightly sweep from Task 3.3 loads every item and asks each one NeedsReorder(). The ConsoleNotifier prints Low stock alert: item SCR-08 is at 10 units and Low stock alert: item DRL-18 is at 2 units, followed by Alerts sent: 2. SCR-08 sits exactly at its threshold of 10, which is why Task 2.1 asked for <=.

    The report from Task 4.3 finds the same two items without calling NeedsReorder() at all. It reads the rows your projection from Task 4.2 stored, with the most depleted item first: DRL-18 with a quantity of 2 and a suggested reorder of 10, then SCR-08 with 10 and 40. Then one command goes through your StockCommandHandler from Task 4.1: Command: received 40 of SCR-08, quantity now 50. The report drops to Report rows: 1. The handler refreshed the projection as part of the write, so the report never had to recompute anything. ### Move the reorder line in one place

    Open src/Domain/StockItem.cs and find the comparison in NeedsReorder(). If you followed Task 2.1, it reads:

    return Quantity <= ReorderThreshold;
    

    Change <= to <, save, and rerun the tour. SCR-08 disappears from both sides: the sweep shows Alerts sent: 1, and the first report shows Report rows: 1. You edited one line in the domain, and a use case on the command side and a read model on the query side both changed. You didn't touch LowStockAlertService or LowStockProjectionStore, because neither one owns the rule. They both ask the entity.

    Put <= back before you finish. To confirm every check still passes, run dotnet test tests/Lab.Tests.csproj --nologo --verbosity quiet.

    ShelfSync's reorder rule now lives in one plain class. The database, the notifier, and the report are details plugged in around it.

About the author

Pluralsight’s AI authoring technology is designed to accelerate the creation of hands-on, technical learning experiences. Serving as a first-pass content generator, it produces structured lab drafts aligned to learning objectives defined by Pluralsight’s Curriculum team. Each lab is then enhanced by our Content team, who configure the environments, refine instructions, and conduct rigorous technical and quality reviews. The result is a collaboration between artificial intelligence and human expertise, where AI supports scale and efficiency, and Pluralsight experts ensure accuracy, relevance, and instructional quality, helping learners build practical skills with confidence.

Real skill practice before real-world application

Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.

Learn by doing

Engage hands-on with the tools and technologies you’re learning. You pick the skill, we provide the credentials and environment.

Follow your guide

All labs have detailed instructions and objectives, guiding you through the learning process and ensuring you understand every step.

Turn time into mastery

On average, you retain 75% more of your learning if you take time to practice. Hands-on labs set you up for success to make those skills stick.

Get started with Pluralsight