Test-Driven Development (TDD) & Clean Architecture in Umbraco: Decoupling Domain Logic from CMS Dependencies

Test-Driven Development (TDD) & Clean Architecture in Umbraco: Decoupling Domain Logic from CMS Dependencies

Umbraco

Umbraco makes it easy to ship fast. It also makes it easy to end up with business logic scattered across content models, controllers, and view templates, the kind of code nobody wants to touch six months later. The instant those rules become part of a document type or Razor view, you have created an integration between your business and your CMS that is difficult to test, and even more difficult to reverse.

While Clean Architecture and TDD have been around for some time, it just so happens that they provide a solution to a very Umbraco-specific problem: how do you keep your true business logic separate from a CMS that wasn’t really meant for it? In this post I’ll show you what that means in practice, and why testing-first changes everything.

Why Umbraco projects get hard to test

  • Business logic often ends up inside controllers or view components because that's the fastest place to write it
  • Domain rules get coupled to IPublishedContent or Umbraco's content models, so you can't test them without spinning up an entire CMS context
  • Integration tests become the default because unit tests aren't practical when everything depends on Umbraco's runtime
  • Every CMS upgrade risks breaking business logic that had no reason to be tied to Umbraco in the first place
  • New developers can't tell where "the rules" actually live, because they're spread across five different layers

What Clean Architecture actually means in an Umbraco context

Clean Architecture isn't about adding more folders. It's about drawing a hard line between what your business does and how Umbraco happens to store and render it. A practical breakdown looks like:

  • Domain layer: your core business rules and entities, with zero references to Umbraco, Umbraco.Cms.Core, or anything CMS-specific
  • Application layer: use cases and orchestration, what actually happens when a user does something still with no CMS dependency
  • Infrastructure layer: where Umbraco lives. Content services, published content queries, and anything touching IPublishedContent sit here
  • Presentation layer: controllers, views, and razor views which make calls into the application layer and display the output

Dependency rule that allows for this: The outer layers may depend upon inner layers but not vice versa. Umbraco is the outer layer. Your domain logic doesn't know Umbraco exists.

Why TDD fits naturally into this

Unit tests, in turn, automatically require the separation of concerns that Clean Architecture demands.

  • If the process of calculating discounts is embedded in a controller that also retrieves content from Umbraco, then it's impossible to create unit tests for its functionality until it's been extracted.
  • And red-green-refactoring cycles work perfectly fine for business and application logic, as there is no CMS context to mock and instantiate.
  • Tests written against interfaces (not concrete Umbraco services) stay stable even when Umbraco's APIs change between major versions
  • Fast, in-memory test runs become normal again, instead of every test needing a database or a running Umbraco instance
  • Bugs in business rules get caught before a single content editor or front-end template is involved

Decoupling domain logic from Umbraco dependencies

This is the part teams skip under deadline pressure, and it's the part that pays off later.

  • Define interfaces in the domain or application layer for anything that needs data, a IProductRepository, not a direct call to Umbraco's content cache
  • Let the infrastructure layer implement those interfaces using actual Umbraco APIs, content models, or Examine queries
  • Map Umbraco content types into plain domain models as early as possible, so nothing downstream ever touches IPublishedContent directly
  • Ensure validation and business logic are kept within domain objects or application services and not within the document type, view model or controller method
  • Wire up infrastructure components using dependency injection such that the domain is only aware of the interface and not the concrete component implementation
  • Treat any direct reference to Umbraco namespaces inside your domain project as a sign the boundary has already been crossed

A practical testing strategy for this setup

  • Domain layer: pure unit tests, no mocking framework needed most of the time, because there's nothing external to fake
  • Application layer: unit tests with mocked repository interfaces, verifying orchestration logic and use cases in isolation
  • Infrastructure layer: integration tests that actually exercise Umbraco content queries, examine search, published content access
  • Presentation layer: thinner tests here, mostly checking that controllers call the right application services and handle results correctly
  • Run domain and application tests in CI on every commit; save the slower Umbraco integration tests for a separate pipeline stage

Best practices

  • Start every feature by asking where the business rule belongs, not where it's easiest to write
  • Resist the urge to inject IPublishedContent into anything outside the infrastructure layer, even when it feels faster
  • Keep domain and application test suites fast enough that nobody skips running them locally
  • Review pulls requests specifically for CMS leakage, a domain class importing an Umbraco namespace is a code smell, not a shortcut
  • Revisit this separation during Umbraco major version upgrades; it's usually where the payoff becomes obvious
  • Document the boundary once, clearly, so new team members don't reintroduce coupling out of habit

Where this leaves your codebase

None of this is about avoiding Umbraco or working around it. It's about giving your business logic a life of its own, one that isn't tied to how your CMS renders a page or stores a node. When domain rules live independently, they get easier to test, easier to reason about, and far easier to carry forward when Umbraco itself changes.

The upfront cost is real. Defining interfaces, mapping content models, and setting up separate test suites takes longer than writing everything inline. But teams that make this investment early spend far less time untangling business logic from CMS internals later — and that trade-off tends to look better with every passing sprint.

If your Umbraco application has outgrown the stage where adding something “just to the controller” is no longer sufficient, perhaps it is time to define that line. Contact Arroact on arroact.com/contact to discuss how this can be done in your case.

Written by
Nishant Bio Image

Nishant Vaghasiya

Technical Architect

I'm Nishant Vaghasiya, a Technical Architect and Umbraco & Sitecore Certified Developer at Arroact Technologies. I specialise in building digital solutions with Umbraco and Sitecore that are practical, scalable, and built to last.

Over the years, I've learned that the best solutions aren't always the most complex ones, they're the ones that make a team's day-to-day work simpler and give them confidence that the system won't let them down.

That's what drives me writing code that performs well, stays reliable, and continues to create real impact long after it goes live.

Related Blogs blue-line-vector-3

How Much Does an Umbraco 13 to 17 Upgrade Cost?
09 September 2610 min read
Umbraco
How Much Does an Umbraco 13 to 17 Upgrade Cost?
Most agencies won't give you a straight answer. They'll say, "it depends" and leave you to…
Read More
Umbraco 18 Elements: A New Way to Manage Reusable Content
10 July 2612 min read
Umbraco
Umbraco 18 Elements: A New Way to Manage Reusable Content
Every Umbraco developer knows the "Settings" node. Buried at the bottom of the content tre…
Read More
Umbraco Delivery API vs. The New GraphQL Package: Choosing the Right Headless Strategy
07 July 268 min read
Umbraco
Umbraco Delivery API vs. The New GraphQL Package: Choosing the Right Headless Strategy
Introduction Headless Umbraco just got more interesting. For years, the Content Delivery A…
Read More