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

Clean Architecture Faces Scale Tests and Change

A ticketing service runs ticket sales and refunds for a growing slate of live events out of a single .NET service. Leadership, spooked by a bad launch elsewhere, wants Catalog, Pricing, and Fulfillment split into microservices before the next on-sale. The engineering team suspects that's premature: the real problems are a payment gateway called directly from business logic, a refund handler nobody can test without hitting a live API, and UI tests so brittle nobody trusts them. Over this lab you rebuild the refund path context by context -- drawing the boundaries first, hiding the payment call behind an interface, isolating the refund command's dependencies, proving the workflow in memory, and keeping the technology choice for notifications open -- so the team can make the microservices call on evidence instead of fear.

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: Orientation

    Step 1: Orientation

    A ticketing service sells tickets and processes refunds for a growing slate of live events out of one .NET service. The refund workflow -- the path from "a customer wants their money back" to "the payment gateway confirms it and the customer is told" -- currently runs through business logic that calls the payment gateway's HTTP client directly, calls a notification service directly, and mixes catalog, pricing, and fulfillment concerns in the same methods. Nobody can test a refund without hitting a live payment API, and the UI tests that exist are so brittle the team routinely ignores their failures.

    Leadership, rattled by a bad launch at a competitor, wants Catalog, Pricing, and Fulfillment split into separate microservices before the next on-sale. The engineering team's suspicion is that this is solving the wrong problem: splitting a tangled monolith into tangled microservices just moves the tangle across a network boundary, and now every test that used to be slow is also unreliable. ## What "Clean Architecture" actually buys the team here

    The fix is not a deployment topology. It's making the workflow's internal boundaries explicit and enforceable, so that whichever technology sits behind the boundary -- HTTP gateway, in-memory fake, SMS, email -- the core refund logic doesn't change and doesn't need a live dependency to test. Four ideas do the work:

    • Bounded contexts: which parts of the system are allowed to call which other parts directly, written down as a rule the compiler and tests can enforce, not a diagram everyone forgets.
    • Ports and adapters: the refund logic depends on an interface (IPaymentGateway, INotificationSender), never on HttpClient or a specific vendor SDK.
    • Dependency inversion: the application layer defines the interface it needs; the infrastructure layer implements it. Infrastructure depends on the application, not the other way round.
    • Test doubles and red-green-refactor: with the gateway and notifier behind interfaces, the refund command's logic can be driven by a failing test, made to pass, and refactored -- all without a live payment API. ## What you'll build

    Over the next four steps you rebuild the refund path piece by piece:

    1. Step 2 encodes the Catalog/Pricing/Fulfillment/Payments/Notifications boundaries as a rule the code enforces, plus a workload-based check for whether a context's traffic actually justifies pulling it into its own service.
    2. Step 3 builds the HTTP-backed payment adapter, an in-memory fake behind the same interface, and an application service that only ever sees the interface.
    3. Step 4 builds the refund command handler itself, driving its validation and approval logic through red-green-refactor and verifying notifications with a hand-built double.
    4. Step 5 proves the whole workflow with an in-memory acceptance scenario, then swaps the notification technology underneath it to show the core doesn't care.

    By the end, the team has working code that demonstrates where the boundaries actually are -- evidence leadership can act on, instead of a diagram they can argue about.

    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: Draw bounded contexts before committing to microservices

    Step 2: Draw bounded contexts before committing to microservices

    Before any code talks to a payment gateway, the team needs to agree -- in a form the compiler enforces -- which parts of the refund workflow are allowed to call which other parts directly. Fulfillment triggering a payment refund is expected. Payments reaching back into Pricing to recalculate a quote is not: that's the kind of undeclared coupling that turns a clean split into another tangled monolith, just with a network hop in the middle.

    src/BoundedContextMap.cs already declares the Context enum for Catalog, Pricing, Fulfillment, Payments, and Notifications, and the shape of two methods: IsCallAllowed, the enforceable boundary rule, and ShouldExtractService, a workload-based check for whether a context is actually busy enough to justify its own deployment pipeline. With the call boundaries enforced, the second question is separate: even where a boundary exists, does the traffic through it justify splitting that context into its own service? A context with light load and one owner gains nothing from an independent pipeline -- it just adds a network call and a deployment to babysit. ShouldExtractService turns that judgement into a rule based on daily request volume and team size, so the extraction decision is made from the same kind of evidence as the boundary itself.

  3. Challenge

    Step 3: Put payment communication behind an application interface

    Step 3: Put payment communication behind an application interface

    The monolith's worst habit is business logic that calls the payment gateway's HTTP client directly -- which means testing a refund means hitting a live API. The fix is IPaymentGateway, already declared in src/PaymentGatewayAdapter.cs alongside the adapter that implements it against the real gateway over HTTP. Nothing above this adapter is allowed to know it's HTTP underneath. To prove the refund logic never actually needed the network, src/FakePaymentGateway.cs implements the same IPaymentGateway interface entirely in memory, keeping a list of refunds it was asked to process instead of sending anything over HTTP. This is the fake the rest of the lab's tests will inject wherever the real adapter would otherwise be required. Finally, src/RefundApplicationService.cs shows what dependency inversion buys the application layer: it takes an IPaymentGateway in its constructor and never asks what's behind it. Whether that's the HTTP adapter or the in-memory fake is a decision made entirely outside this class.

  4. Challenge

    Step 4: Isolate command dependencies with red-green-refactor

    Step 4: Isolate command dependencies with red-green-refactor

    The refund command is where validation, payment, and notification meet, and it's exactly the kind of logic that's easy to get subtly wrong under pressure -- approving a refund that's out of the return window, or notifying a customer before the money has actually moved. src/RefundCommandHandler.cs declares RefundRequest, the INotificationSender interface, and a handler that takes both IPaymentGateway and INotificationSender through its constructor, so every collaborator can be faked in a test. With validation in place, ApproveRefund is the method that sequences everything: check the request, move the money, tell the customer -- and only in that order, because notifying about a refund that failed is worse than not notifying at all. Verifying that a notification actually fired needs something to inspect afterward -- a real INotificationSender would send an actual message with no way to check it. src/FakeNotificationSender.cs is a hand-built double that keeps every call it received in a list instead, which is the whole point of a test double: observable state in place of a side effect.

  5. Challenge

    Step 5: Run acceptance scenarios in memory and defer technology choices

    Step 5: Run acceptance scenarios in memory and defer technology choices

    An acceptance test that proves the refund workflow end to end shouldn't need a browser, a UI, or a live payment API -- it needs the application layer wired up with fakes and driven the way a Given/When/Then scenario is written on paper. src/ScenarioExecutor.cs declares ExecuteScenario, the one method every scenario in this lab runs through: given a handler and a request, it runs the workflow and reports whether the outcome matched what was expected. src/RefundAcceptanceScenarios.cs uses that executor to prove the customer-facing scenario that matters most: a valid, in-window refund request should be approved. Building it entirely from fakes is what makes it an acceptance test the team can run on every commit, not a manual click-through before a release. The last piece of evidence is that the notification technology was never a decision the core needed to make early. src/SmsNotificationAdapter.cs implements the same INotificationSender interface the fake and the handler already depend on, but through an SMS-shaped channel instead. Swapping it in for the fake proves RefundCommandHandler behaves identically either way -- the technology choice stayed deferred until it was actually needed.

  6. Challenge

    Step 6: Watch the refund workflow hold together

    Every task is done and graded. Now watch the pieces you wrote separately behave as one refund workflow. You will run the whole thing, see a refund rejected before it reaches payment, and see the same handler work with a different notification technology. ### See the boundaries and the evidence check decide

    Open the Terminal tab and run:

    dotnet run --project src/Lab.csproj
    

    Find the two sections near the top. Under Task 2.1, Fulfillment -> Payments allowed? True but Catalog -> Payments allowed? False. That is your IsCallAllowed rule: a declared dependency passes and an undeclared one fails loudly.

    Under Task 2.2, Volume=50000, Team=12 -> extract? False, even with a team of 12. Your ShouldExtractService requires volume to exceed 50,000, so a context sitting exactly at the threshold is not yet a case for a microservice.

    Try it: in src/Program.cs, change ShouldExtractService(50000, 12) to ShouldExtractService(50001, 12), run again, and the line now reads extract? True. Change it back when you're done. ### Watch a bad refund stop before payment

    In the output, find the Task 4.1 & 4.2 section. The valid request (5 days since purchase) gives ApproveRefund (valid request) approved: True and Notifications sent so far: 1. The stale request (400 days) gives:

    ApproveRefund (stale request) approved: False
    Refunds recorded: 1, notifications sent: 1
    

    Both counts are still 1, the numbers from the valid request. The stale one never reached the fake payment gateway and never triggered a notification. That is your ValidateRefundRequest from Task 4.1 guarding the sequence in your ApproveRefund from Task 4.2, and you can see it without a live payment API. ### Swap the notification technology under the same handler

    The last two sections prove the workflow in memory and then change technology. Under Task 5.2, RunCustomerRefundScenario passed: True ran the full refund through your fakes with no browser or server. Under Task 5.3, the same RefundCommandHandler is built with SmsNotificationAdapter instead of the fake sender:

    ApproveRefund using SmsNotificationAdapter approved: True
    SMS texts recorded: 1
      order-600: Your refund has been approved.
    

    The handler's code did not change. Only the object behind INotificationSender did. That is the dependency inversion the lab was about, and it is what lets the team defer a technology decision until the workload actually asks for it.

    To rerun the graded checks at any time:

    dotnet test tests/Lab.Tests.csproj --nologo --verbosity quiet
    

    You now have a refund workflow with enforced boundaries, a payment call hidden behind an interface, and a core you can test and re-wire without touching a live dependency.

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