Orchestration vs Domain Code Testing

During a recent code review with a co-worker, we were discussing ways of making some code more testable, and stumbled across the concept of “orchestrating” code and “domain” code. I thought it was one of the more clever concepts we had talked about recently, so figured it’d be a good subject for a blog post!

Take this method as an example:

public Invoice CreateInvoice(Customer customer)
{
    var pendingInvoiceItems = _dataLayer.GetPendingInvoiceItems(customer.Id);
    ITaxTable taxTable;
    if(!customer.IsTaxExempt)
    {
        taxTable = Constants.EmptyTaxTable;
    }
    else
    {
        taxTable = _dataLayer.GetTaxTable(customer.TaxJurisdictionId);
    }

    var taxes = new List<Tax>();
    foreach(var invoiceItem in pendingInvoiceItems)
    {
        foreach(var tax in taxTable.GetTaxesForCategory(invoiceItem.TaxCategoryId))
        {
            var taxAmount = tax.Rate * invoiceItem.TaxableAmount;
            taxes.Add(new Tax(rate.TaxId, taxAmount));
        }
    }

    var invoice = _dataLayer.CreateInvoice(invoiceItems, taxes);
    return invoice;
}

This is a perfectly reasonable method, and probably one that looks similar to code you may work with in your various domains. We retrieve some data, perform some calculations on it, persist it to the data store, and then return the result. The thing we were focusing on was how testable the method is.

To me, this method is doing two things; orchestrating, and applying domain logic. Orchestration in this case refers to the fact that it’s calling other methods in a specific order and based on certain criteria. We’re loading pending invoice items, potentially loading a tax table, and persisting the calculation result to a database. These are all separate units of work that are called in a specific order and conditionally based on the purpose of the CreateInvoice method. All great, and something you’d want to unit test to ensure it continues to happen in the order and with the conditions you specify.

The issue is that we’re mixing in domain logic, which by itself should be considered a discrete chunk of testable code. In the example, the domain logic is how we’re taking the information we fetched and applying a calculation to it. This is discrete, valuable, testable code that represents the business value we provide to our users. It’s important! Almost anyone can string a bunch of methods together; the place where our software provides value is in the specific manipulation of data according to business requirements. This is something you should most certainly unit test!

A more concrete argument for this (and where the conversation with my co-worker and I originally started) is how testable this code is. If I wanted to test the domain logic - the tax calculation part - I need to mock out both the data retrieval and data persistence methods. And conversely, if I wanted to ensure my data methods were being called with the appropriate conditions and in the right order, I’d have to provide enough data that the tax calculation code didn’t throw an exception. Both of these things are outside of the scope of the thing I want to test in either case.

This is an issue I see in a lot of code bases, the fact that unit tests become such a pain to set up because there’s too much going on in the method. And of course, if unit tests are hard / time-consuming to write, they become less of a priority for developers and software teams; your code becomes less testable, bugs creep in, and customers are ultimately affected (either through increased release times due to extended manual testing and bug fixing or, in a worse-case-scenario, bugs presented to end-users).

So, how would we clean this up? Pretty simple.

public Invoice CreateInvoice(Customer customer)
{
    var pendingInvoiceItems = _dataLayer.GetPendingInvoiceItems(customer.Id);
    IEnuemrable<Tax> taxes;
    if(!customer.IsTaxExempt)
    {
        taxes = null;
    }
    else
    {
        var taxTable = _dataLayer.GetTaxTable(customer.TaxJurisdictionId);
        taxes = _taxCalculator.GetTaxesForInvoiceItems(pendingInvoiceItems, taxTable);
    }
 
    var invoice = _dataLayer.CreateInvoice(invoiceItems, taxes);
    return invoice;
}

//and in another class
public IEnumerable<Tax> GetTaxesForInvoiceItems(IEnumerable<InvoiceItem> pendingInvoiceItems, 
    ITaxTable taxTable)
{
    var taxes = new List<Tax>();
    foreach(var invoiceItem in pendingInvoiceItems)
    {
        foreach(var tax in taxTable.GetTaxesForCategory(invoiceItem.TaxCategoryId))
        {
            var taxAmount = tax.Rate * invoiceItem.TaxableAmount;
            taxes.Add(new Tax(rate.TaxId, invoiceItem.Id, taxAmount));
        }
    }
    return taxes;
}

Now we have a method that only orchestrates (calls other methods in a specific order / with specific conditions) that is very unit testable, and we have a separate method that only has domain logic that is very unit testable. Neither unit test would require any more setup than is explicitly required for the scenarios they’re testing.