How to know if your Domain model is properly isolated?



In this post, I’ll show a simple way to get to know if your domain model is properly isolated.

Domain model isolation

As I wrote previously, it’s a good idea to construct entities and value objects in a way that allows them not to depend on any external logic, such as the work with a database, 3rd party services, and so on. The main benefit you achieve by doing so is proper separation of concerns which in turn makes your code base easier to reason about.

In many cases, it’s pretty easy to say if this guideline is violated or not. Sometimes, however, it might not be a simple task.

Let’s take an example with a user and an email attribute. Assume you need to change the user’s email:

public class User : Entity

{

    public string Email { get; protected set; }

 

    public void UpdateEmail(string newEmail)

    {

        if (!string.IsNullOrEmpty(newEmail))

            throw new InvalidOperationException();

 

        Email = newEmail;

    }

}

In terms of domain model isolation, this implementation is obviously good: the UpdateEmail method is closed under primitive types and doesn’t depend on external concerns.

Now, let’s say we need to ensure email uniqueness while changing it. This requirement should not be a responsibility of the User entity but let’s implement it like this anyway for the sake of argument:

public class User : Entity

{

    public string Email { get; protected set; }

 

    public void UpdateEmail(string newEmail)

    {

        User user = Database.GetUserByEmail(newEmail);

 

        if (user != null && user != this)

            throw new InvalidOperationException();

 

        Email = newEmail;

    }

}

Here, the user gets an implicit dependency on the database, and this dependency breaks the isolation. Now, in order to reason about this class, we need to consider two unrelated concerns simultaneously: the domain logic itself and the work with the database.

Alright, how about this one?

public class User : Entity

{

    public string Email { get; protected set; }

 

    public void UpdateEmail(string newEmail, Database database)

    {

        User user = database.GetUserByEmail(newEmail);

 

        if (user != null && user != this)

            throw new InvalidOperationException();

 

        Email = newEmail;

    }

}

The external dependency is still there, we just made it explicit. The domain model is still not properly isolated.

Now, what if we replace the Database class with an interface? Doesn’t it mean we abstract that dependency out and make User pure again? Actually, no, it doesn’t. As I mentioned previously, in order for an interface to be a genuine abstraction, it should represent a concept meaningful for your domain. The work with a database is not one of such concepts, it’s just a persistence implementation detail.

Nice try, interface, but no.

Here’s another version:

public class User : Entity

{

    public string Email { get; protected set; }

 

    public void UpdateEmail(string newEmail, Func<string, bool> isEmailUnique)

    {

        if (Email == newEmail)

            return;

 

        if (!isEmailUnique(newEmail))

            throw new InvalidOperationException();

 

        Email = newEmail;

    }

}

Okay, now this becomes confusing. Is this implementation adhering to the isolation principles? At first glance, yes, as there’s no direct dependency on the database. The delegate we pass into the method doesn’t tell us anything about the database either, it just takes a string and returns a boolean.

But is User really isolated taking into account that we do know the actual delegate we feed into the method talks to the database?

Identifying if your Domain model is properly isolated

There is a quite simple technique that helps answer this question and identify if your domain model is properly isolated.

As many other good ideas, this one comes from the world of functional programming. To apply it, we need to resort to the notion of method signature honesty. That is, we need to look at all methods involved in our domain model and explicitly state in their signatures the range of input values they accept and the possible outcomes they may produce.

Unfortunately, neither C# nor F# allows us to achieve the desired level of honesty when it comes to the work with the external world, so we need to take Haskell. Haskell’s type system is incredibly strict. If you’ve got a function that works with any kind of IO – be it the file system, network, or a database – the language enforces you to state that fact in the function’s signature. And that’s exactly what we need here.

I’m not going to write the actual Haskell code, though. Haskell is a great language but I don’t feel fluent in it, so what I’m gonna do instead is I’m going to introduce C# code which would be analogous to Haskell in terms of the work with IO. Keep in mind that C# doesn’t really have such a construct, though. Our exercise will be a purely imaginary one.

Alright, so how would the following code look like should we lift all the details regarding its inner workings to the signature level?

public class User : Entity

{

    public string Email { get; protected set; }

 

    public void UpdateEmail(string newEmail, IDatabase database)

    {

        User user = database.GetUserByEmail(newEmail);

 

        if (user != null && user != this)

            throw new InvalidOperationException();

 

        Email = newEmail;

    }

}

 

public interface IDatabase

{

    User GetUserByEmail(string newEmail);

}

First of all, the IDatabase interface would look like this:

public interface IDatabase

{

    IO<User> GetUserByEmail(string newEmail);

}

Note we changed the return type from User to IO<User>. This change is mandatory due to the fact that the implementation of this interface talks to the database and is enforced by Haskell’s type system. If we were to leave the interface as before, we wouldn’t be able to pass into UpdateEmail any implementations of that interface that do work with the database, they simply wouldn’t be compatible.

In the world of strictly enforced method signature honesty, functions of type

string -> IO<User>

are not convertible to functions of type

string -> User

Having a dependency with such a method signature, the User class now must enter a special IO context in order to get a user from the database:

public IO<unit> UpdateEmail(string newEmail, IDatabase database)

{

    User user = IO { database.GetUserByEmail(newEmail); }

 

    if (user != null && user != this)

        throw new InvalidOperationException();

 

    Email = newEmail;

}

As you can see, the impurity in our domain model becomes obvious: the presence of the special IO context tells us about that. Moreover, the signature of the UpdateEmail method itself also reveals the impurity. Note that the return type has been changed from void to IO<unit>. In Haskell, only impure methods can work with other impure methods, so any method that uses the IO context must also state that fact in their signatures.

This kind of chain reaction is often referred to as “impurity infection”. A single sub-function that employs any type of IO infects the whole wrapping function and makes it impure as well. This process is very similar to what we have with async operations in C#. If you’ve got an async method, you have to make the method calling it async too and change its return type from T to Task<T>. Admittedly, though, in C#, we have a couple of hacks that allow us to break out of the Task monad.

By the way, if you wonder what this unit from the return type is, it’s an FP analog for void. Unlike void, however, you can pass unit further to other methods, and that fosters composability.

Alright, introducing an interface doesn’t help us with adhering to the domain model isolation principles. But that’s something we knew already. What about the delegate? Here it is again:

public void UpdateEmail(string newEmail, Func<string, bool> isEmailUnique)

{

    if (Email == newEmail)

        return;

 

    if (!isEmailUnique(newEmail))

        throw new InvalidOperationException();

 

    Email = newEmail;

}

To see if this code doesn’t bring any external dependencies, we need to “haskellify” it too. How would the delegate’s signature look like should we bring honesty to it? It would look like this:

Func<string, IO<bool>> isEmailUnique

Once again, if we leave the delegate as before, we wouldn’t be able to use a method that actually refers to the database in place of that delegate, Haskell simply forbids us from doing so. We must specify the work with IO explicitly in the delegate’s signature.

Therefore, in order to invoke the delegate, we should enter the IO context here too:

public IO<unit> UpdateEmail(string newEmail, Func<string, IO<bool>> isEmailUnique)

{

    if (Email == newEmail)

        return;

 

    bool unique = IO { isEmailUnique(newEmail); }

 

    if (!unique)

        throw new InvalidOperationException();

 

    Email = newEmail;

}

And that, as you can see, once again makes the UpdateEmail method impure which is explicitly manifested by its return type.

Conclusions

So back to the initial question regarding the delegate and the domain model isolation. Although C# allows us to hide the work with the IO behind the scenes, it doesn’t really change the fact that such delegate introduces impurity to our domain model. And we can clearly see that if we make the delegate’s signature completely honest using the haskellifying technique. If after applying this technique, any method in your domain entities and value objects contain IO in their signatures, your domain model is not properly isolated.

Another way to answer this question is to re-state it like this: are you able to unit test a method without involving any sort of a test double? If the answer is no, it also means your domain model is not protected from the external influence. In the above example with the delegate (the initial version of it), we have to use a stub in order to substitute the actual call to the database.

Unfortunately, there’s no way around this. The versions with the interface and the delegate are merely hacks and don’t help with achieving genuine domain model isolation. They rely on the mildness of the C#’s type system which allows you to hide the work with external dependencies from the eyes of the client code.

UPDATE

I need to add a remark here. There is a dichotomy between the domain model “wholeness” and its purity. On one hand, the isEmailUnique delegate talks the language of the domain, and excluding it from the domain entity can potentially result in incompleteness (although this can be mitigated by extracting that logic into a domain service). On the other hand, the delegate introduces impurity. It’s up to the developers to decide where they want to lean on this spectrum. I personally lean towards purity but I wouldn’t say it’s always justified and I understand arguments for the other part of this spectrum.

Summary

To see if your domain model isn’t influenced by the external world, you need to bring honesty to all methods inside your entities and value objects.

  • Apply the haskellifying technique: if any method works with any kind of IO (database, file system, etc.) state it in the method’s return type. Methods that work on top of it become impure too.
  • After haskellifying your code, look at entities and value objects. Do methods inside them have IO in their signatures? If yes, it means your domain model is not properly isolated.

Related articles

Share




  • Douglas Hammon

    Just wanted to let you know I’ve been really enjoying your blog posts. The DDD info is especialy usful. Keep up the good work!

    • http://enterprisecraftsmanship.com/ Vladimir Khorikov

      Thanks!

  • Andrew Starikov

    Hello Vladimir.
    Your article was very interesting to me, but knowing Java and not knowing .Net I have two questions:

    1) can this IO { … } block be implemented in C#? how will this syntax construct be called?
    2) can it be enforced on the compilation level that if the code inside method A uses IO { … } then method A itself MUST return IO and not just an arbitrary type? if yes, could you then outline how it works?

    If the answers to both questions are “yes”, then I’d say wow it’s a great technique. I’ve been thinking about the same for Java but haven’t come up with any enforcing technique.
    Thank you!

    • http://enterprisecraftsmanship.com/ Vladimir Khorikov

      Hi Andrew,

      Unfortunately, C# doesn’t really support this structure. The IO block and the IO generic I introduced in the code samples don’t exist in the language. The only language I know of supporting this is Haskell.

      Basically, I just wanted to show how the IO monad would look like in C# should it existed there, hence I used IO { … } and IO<T> to do that. But that’s a purely imaginary exercise and neither of these language constructs exist.

  • Alexandre Potvin Latreille

    It’s an interesting read and it’s a bit ironic, but I find haskellified abstractions extremely impure. Abstractions shouldn’t depend on details. What if `isEmailUnique` uses an in-memory store? Your explicit IO contract doesn’t make sense anymore. As long as abstractions are created with the Ubiquitous Language in mind and do not depend on details I do not see how they would be impure or how that “impurity” would have a negative impact on the design?

    From the client perspective (domain model in this case), `isEmailUnique` answers a simple business question. How it does it should be completely irrelevant, no? However, one should always be aware that any data external to an aggregate could be stale.

    To me the impurity is `UpdateEmail` which sounds very CRUDdy and `InvalidOperationException` which is certainly not part of the UL. `EmailUniquenessPolicyViolation` already sounds better.

    • http://enterprisecraftsmanship.com/ Vladimir Khorikov

      Some great points here!

      What if `isEmailUnique` uses an in-memory store? Your explicit IO contract doesn’t make sense anymore.

      It does. Functions with signature `string -> IO<User>` cannot be converted to functions of type `string -> User`, that’s something I mentioned in the post but otherwise is not the case, you can use a `string -> User` function in place where `string -> IO<User>` is required. So you can still use an in-memory store here.

      You bring an important concern, however. That is the dichotomy between the domain model “wholeness” and its purity. The `isEmailUnique` delegate indeed talks the language of the domain. On the other hand, it brings impurity to it. It’s up to the developers to decide where they want to lean on this spectrum. I personally lean towards purity but I wouldn’t say it’s always justified and I understand arguments for the other part of this spectrum.

      Regarding your suggestion that EmailUniquenessPolicyViolation sounds better than InvalidOperationException – that’s only if you rely on exceptions to control the program flow, which in my opinion is a bad idea. If you throw exceptions to signalize a bug in your software, there’s no difference in what exact exception you throw, they all should terminate the current operation regardless of the exception type (Fail Fast principle).

      • Alexandre Potvin Latreille

        I’m sorry, I had not read the other article 😉 It makes more sense now. As for the exceptions, do you consider an attempt to violate a business rule a bug? I guess in some scenarios it could be, but it could also be the result of an operation that appeared valid at first because the decision was taken based on stale data. Unless you have completely real-time user interfaces (few seconds old data at most) it is a scenario that is not really exceptionnal. I’d be tempted to use domain events to communicate such failures instead of exceptions, but I never tried it, is it what you do?

        Also, to come back to the “wholeness” of the domain, I only inject such services at the method level when resolving the dependencies would result in leaking moderately complex business logic into the application layer. Otherwise I agree that these are better resolved in application services. For instance, I’d probably opt for something like `emailUniquenessPolicy.enforceFor(email)` in the application layer (which could write to a DB for strong consistency).

        • http://enterprisecraftsmanship.com/ Vladimir Khorikov

          but it could also be the result of an operation that appeared valid at first because the decision was taken based on stale data

          In this case, I prefer to use a return value signalizing that the operation has failed, namely some sort of a Result class. In general, failures can be separated into 2 categories: expected and unexpected. For expected failures, I’d recommend using return values and not exceptions; for unexpected – exceptions which you allow to propagate and fail the current operation completely. Here I wrote about the reasoning behind this in more detail: http://enterprisecraftsmanship.com/2015/02/26/exceptions-for-flow-control-in-c/

  • Adrian Edwards

    While not a solution by any means, it is worth mentioning that the (under-appreciated) Code Contracts library (now System.Diagnostics.Contracts) can offer some help with this, as it includes a [Pure] attribute intended for marking methods that “do not make any visible state changes.”

    https://msdn.microsoft.com/en-us/library/system.diagnostics.contracts.pureattribute(v=vs.110).aspx

    Unfortunately, “[t]his attribute is not enforced by the current analysis tools.” But it is a start!

    • http://enterprisecraftsmanship.com/ Vladimir Khorikov

      Good point! Hopefully, Code Contracts will become part of the language someday.

  • xiety

    I personally lean towards purity

    Can you provide an example of UpdateEmail method with such purity?