Domain model isolation

By Vladimir Khorikov

I’ve been using the term “domain model isolation” for a long time already but just recently realized that its meaning might not be as obvious as I thought. In this post, I’ll try to describe what it means to properly isolate your domain model and why it is important.

Domain model isolation

The notion of domain model isolation is very similar to what functional architecture proposes: you make the code that comprises the core part of your application pure while pushing the side effects and the work with external dependencies (such as the database) to the edges of a business operation, either to the beginning or to the end of it. The immutable core in that sense is the domain model. The mutable shell is the application services layer.

There’s more to it, however, and the notion of immutable core in functional architecture does not fully correspond to an isolated domain model. In other words, a domain model can be fully isolated but at the same time – not entirely pure in a functional sense. The opposite situation can also be the case:

Domain model isolation: Immutable Core vs Isolated Domain Model

Immutable Core vs Isolated Domain Model

The main difference between the two is in how they regard immutability. While in functional programming side effects tend to be pushed away from the immutable core entirely, DDD in general doesn’t prohibit them as long as they are restricted by the boundary of the domain model.

To put it differently, an operation that is invoked on a domain model can modify the state of the objects in it, but should not in itself result in a change somewhere in the file system or in a 3rd party API. That’s what your application services are for. As I wrote in the previous post, the domain model is responsible for making business decisions while the app services layer converts those decisions into visible bits, such as changes in the database.

What makes a domain model isolated?

The easiest way to determine if your domain model is fully isolated is to diagram entities and value objects in it along with the classes from the surrounding world. If those entities and value objects talk to any classes other than their neighbor entities and value objects, your domain model lacks isolation:

Domain model isolation: Example of a leaking Domain Model

Example of a leaking Domain Model

Here, for example, the relationship between Person and Address is fine and doesn’t contradict the concept of domain model isolation. However, you can also see that the Person entity refers to the corresponding repository, and the Address value object uses a location API. Both of these two classes are gateways to the external world, and thus interactions with them break the domain model’s isolation.

Another way of looking at it is using the notion of onion architecture. If we put the classes above to appropriate layers in an onion, here’s what we’ll get:

Domain model isolation from an onion architecture perspective

Domain model isolation from an onion architecture perspective

The relations you see on this diagram point upwards, from the inner core of the onion to the external layers. At the same time, the onion architecture tells us that all communications should go in the backward direction only, from the upper layers to the inner ones. Violation of this guideline tells us that the domain model is leaking.

An isolated domain model is a closed one. All operations on the domain model should be closed under its entities and value objects. In other words, the arguments of those operations, be they explicit or implicit, and their return values should only consist of primitive types or the domain classes themselves.

Here’s an example illustrating this idea:

public sealed class Address : ValueObject<Address>

{

    public string AddressString { get; }

    public string ZipCode { get; }

 

    public Address(string addressString)

    {

        AddressString = addressString;

        ZipCode = new LocationApi().LookupZipCode(addressString);

    }

}

The Address value object accepts an address string and uses a Location API to look up corresponding zip codes. The LocationApi class here is an implicit argument to the constructor. It’s not a domain class, nor it is a primitive. Therefore, this implementation of the domain model is not closed under itself, it is not isolated.

Note that the notion of isolation is only applicable to entities and value objects. They are the heart of any domain model. Other domain classes, such as factories, domain services, and, obviously, repositories can and usually do refer to the external world.

Another example is the Active Record pattern:

public class User : Entity

{

    public void Save()

    {

        /* Saves itself to the database */

    }

 

    public void Load(int id)

    {

        /* Loads itself from the database */

    }

}

Entities implementing this pattern work with the database directly in order to save or load themselves to and from the database. The implicit argument they use to do that – the database – also breaks the closure.

Domain model isolation and dependency injection

Let’s look at a common practice for getting around this whole isolation issue. I sometimes see people agreeing that the following code has a smell:

public sealed class Address : ValueObject<Address>

{

    public string AddressString { get; }

    public string ZipCode { get; }

 

    public Address(string addressString, LocationApi locationApi)

    {

        AddressString = addressString;

        ZipCode = locationApi.LookupZipCode(addressString);

    }

}

And then immediately proposing a “fix” for that smell. That is, introducing an interface:

public sealed class Address : ValueObject<Address>

{

    public string AddressString { get; }

    public string ZipCode { get; }

 

    public Address(string addressString, ILocationApi locationApi)

    {

        AddressString = addressString;

        ZipCode = locationApi.LookupZipCode(addressString);

    }

}

This, of course, doesn’t make any difference. Introducing an interface for some external concept and injecting it into an entity or value object is just a hack to avoid the effort required to achieve genuine domain model isolation. The reason why it is a hack is that such interfaces don’t represent an abstraction, they are merely a tool to enable unit testing.

You can, of course, apply the practice of dependency injection in the domain model but those dependencies must represent an actual abstraction meaningful to your domain, not something coming from the external world. In many cases, it means that the interfaces you inject to your entities and value objects must be implemented by other entities and value objects. Also, the Principle of Reused Abstractions tells us that, in order to be considered a good abstraction, those interfaces should have more than one implementation. Some programmers even go as far as suggesting that, for each interface, there should be at least 3 classes implementing them.

That’s, by the way, the whole premise behind the Dependency Inversion Principle (DIP). While you can easily implement Dependency Injection using some arbitrarily chosen interfaces, those interfaces don’t automatically follow the DIP principle.

All this combined makes a well-designed and isolated domain model unlikely to have any interfaces “abstracting” entities and value objects. Entities and value objects in themselves represent the problem domain really well, there’s usually no need to abstract them further.

Semantics pollution

Another sign of a domain model closed under itself is when it doesn’t operate classes that come from other domains/bounded contexts. If you’ve got a class that came from an SDK of some third party service, you shouldn’t pass it directly to your domain classes, even if that class in itself is just a bag of data and doesn’t refer to the external world.

The reason is that such external concepts almost always contain data your core domain doesn’t need. This data therefore should be filtered out in order to adhere to the YAGNI principle.

Another reason is invariants. You don’t have control over the promises the external services make and even if you validated them manually once, you can’t be sure they are held at all times. To ensure validity of the data coming from external services, you need to wrap them with your own classes which would be part of your domain and which would explicitly state the invariants you expect the external service to keep.

The same is true for classes coming from other bounded contexts. The best way to isolate your domain model from them is to implement an anti-corruption layer which would translate alien notions into something meaningful to your domain classes.

Note that having an anti-corruption layer, while beneficial in many cases, is not always justified and your domain model might very well live in a partnership relation with other bounded contexts or even have a shared kernel with them.

Why isolate the domain model?

The guidelines above look like a lot of work, so it’s legit to ask the following question: why bother at all? Why put so much effort?

The answer, as always, is to battle complexity. Proper separation of concerns allows you to reduce the cognitive load needed to reason about the code base. The more complex your project gets, the more important that issue becomes. On sufficiently large code bases, it is crucial to be able to think of your domain model in isolation from all other concerns. In many cases, it is effectively the only way you can keep up with the ever-growing complexity of your project.

Another reason is testability. Having a domain model closed under itself makes the process of unit testing trivial. With such a model, you can easily apply output and state verification which give the best return on investment.

Note that full isolation of entities and value objects is not always possible to achieve, and there could be situations where you simply can’t do that. For example, in the face of tight performance requirements, you might have no option other than cutting the corners. Nevertheless, domain model isolation is a good goal to aim for as it provides substantial benefits.

Summary

  • An isolated domain model is a model operations on which are closed under its entities and value objects.
  • The notion of domain model isolation is only applicable to entities and value objects. Other domain classes can talk to the external world.
  • This concept is similar to the notion of immutable core from Functional Architecture. The main difference is that the former allows for side effects which are restricted by the boundaries of that model.
  • Replacing an external dependency with an interface does not mean you fix the leaking domain model. In order to adhere to the Dependency Inversion Principle, the interface should represent a business-meaningful abstraction in that it should help your domain classes make business decisions.
  • Semantics pollution is another sign your domain model is not properly isolated.
  • Domain model isolation brings two benefits: it reduces code complexity and enables better testability.

Related articles:





  • E.

    Thanks for your insights!
    I’m working on a complex project where I have to convert data from one (external) domain to the business domain of my client. I let create the external domain all the necessary events needed to feed the data for the other domain. Both domains are based on the data from their respective databases. In the internal domain I need to use some data that are kind of ‘Type-tables’, holding only key/value pairs (for example type of cars with ID and CODE…). I treat those tables as value types. But, I need to use them very regularly in the internal domain. For this reason I use caching.
    What is now the best practice for using this caching with DDD? I was first thinking for the use of injecting an ICache interface in the internal domain (aggregate) where I have all cached data present. This way it is possible to test the internals of the domain entity by using another cache. But, in fact this cache is not really belonging to the domain logic… What is the best solution in this case? Is it better to use a service layer that is doing the conversion process then where I use the caching? And where and how do I have to design this service? Or have you other suggestions that might help design an isolated domain model?

    • http://enterprisecraftsmanship.com/ Vladimir Khorikov

      If that key/value data is reference data (data your application doesn’t change on its own), I would recommend adopting this approach: http://enterprisecraftsmanship.com/2016/03/17/reference-data-as-code/ In my experience, it works best for such use cases. It wouldn’t work if it’s regular master data, thought.

      If latter is the case, I would recommend commissioning this type of work to the app services layer. So, an app service would get a car type from the cache, convert it into a value object (or maybe the cache already keeps the data in the form of value objects, then no conversion needed), and feed that value object to the aggregate. The tests then will also operate value objects instead of passing ICache.

      Hopefully that helps. If not, provide a more detailed description/code samples, I’ll try to describe how I would approach the problem in your particular situation.

      • E.

        Thanks for your suggestion to use the concept of “Reference data”. However, I have to create more than 25 of such Entities and they contain also a lot of data. This will be a lot of manual work and if something changes in the future I have to change it in my code too.
        For the caching I’m using a LazyCache : https://github.com/alastairtree/LazyCache
        I created following CacheRepository :
        public abstract class CacheRepository
        where T : Entity
        {
        private static readonly IAppCache _cache = Singleton.UniqueInstance;

        public static List Get()
        {
        return _cache.GetOrAdd(typeof (T).Name, GetAll).ToList();
        }

        private static IList GetAll()
        {
        using (ISession session = SessionFactory.OpenSession())
        {
        return session.Query().ToList();
        }
        }
        }

        For the “reference data” I use following code :
        public class Country_Repository : CacheRepository
        {
        public static Country Get(string code)
        {
        return Get().FirstOrDefault(v => v.CODE == code);
        }
        }

        When inside the aggregateroot I need a specific Country I only have to use :

        Country_Repository.Get(“dataToconvert”);

        But, this way I can’t test the aggregateroot without the use of the cache (because it’s a dependency).

        The purpose of the application is to convert data. The point is now that I can bring the conversion of these “reference data” to a higher level by using the converted-entities in the parameters for the aggregate method. But, this seems to put the responsability no longer inside the aggregateroot while the data like Country is domain-specific…
        I would really appreciate if you could elaborate a little bit more on your approach for this problem if possible.
        Thanks already for your help !

        • http://enterprisecraftsmanship.com/ Vladimir Khorikov

          Correct me if I’m wrong here. So you have a generic cache of data and your domain entities need some specific portions of it (I’m assuming “dataToconvert” in Country_Repository.Get(“dataToconvert”) is some predefined constant).

          If that’s the case, I would really suggest that you apply the approach I described in “Reference data as code”. You can implement it partially and add only those value objects that are used by your domain model directly. You can also use T4 templates for that. So instead of writing

          Country_Repository.Get(“United States”);

          you can do

          Country.UnitedStates

          No need to pass ICache to the aggregate root in this case.

          An alternative solution is to pass a preloaded list of data to the aggregate root from app services. In this case, the code will look like this:

          void DoSomething(IReadOnlyList countries)
          {
          var country = countries.Single(x => x.Code == “United States”);
          /* … */
          }

          This way you still keep the decision making process in the entity, within the domain model boundary.

          The latter approach looks quite cumbersome though, so I would suggest going forward with the former one.

          • E.

            Thanks for your help and quick response!
            The problem is that I don’t know in advance what data will be transferred for conversion. This means, I need all this data ready in my aggregateroot… The “DataToConvert” is in fact the key that I send to the cache and that returns me the value.

            The conversion is a process in which I use an event-handler. It is the event-handler that calls the aggegrate.
            When an event arrives, I know what data is needed for that specific item. So, at that moment I can eventually use the cache and do a translation before calling the aggregate. This means that I have to open the dto’s that arrive in the handler and that I have to convert it to the correct entity; (country for example).
            Or do you think that is a bad idea too?

          • http://enterprisecraftsmanship.com/ Vladimir Khorikov

            If you can perform the conversion on the app service (the event handler), that would be the preferable solution, IMO. So, in the handler, just fetch the data from the repository by the code you’ve received with the dto.

            If you are worrying about leaking domain knowledge into the event handler, I don’t think the act of getting a country by its code is domain logic as it’s not related to making any business decisions. Also, transformations is exactly the thing the app services layer is usually responsible for, so such design looks absolutely fine to me.

          • E.

            Great! Thank you very much for your help.
            I’m looking forward to your next great article.

  • Anders Baumann

    Hi Vladimir.
    Thanks for a great article. As always a pleasure to read.

    Just a comment about the problem with passing LocationApi in the constructor: It seems to me that you left out a good solution. I would suggest to pass the actual zip code (as a string) instead. “Are we getting something from the dependency? If so, pass the thing we’re getting, not the thing that gives it to us.” This advise is from the article: http://qualityisspeed.blogspot.com/2015/02/the-dependency-elimination-principle-a-canonical-example.html. The article suggest to change our default and treat dependencies as code smells. I really like that advise and it has helped me eliminate a lot of unnecessary dependencies from my code.

    Best regards,
    Anders

    • http://enterprisecraftsmanship.com/ Vladimir Khorikov

      I would suggest to pass the actual zip code (as a string) instead.

      Yes, that’s exactly how I would refactor this code too.

      Great article BTW, thanks for sharing!

      • xiety

        But what if domain logic will go to the branch where zip code is not required. There will be a redunant LocationApi call.

        • http://enterprisecraftsmanship.com/ Vladimir Khorikov

          In case of such a branching logic, I would recommend handling it in an app service. Alternatively, you could keep that redundant call as is if doesn’t bring much trouble.

  • Andrey Yurashevich

    Hi Vladimir,

    Thank you for your great blog.

    Probably it’s not related to the topic of this article but this question worries me for a long time. In this article you mentioned that factories, domain services and repositories are domain objects which basically means that they are available to other domain objects like entities and value object. But should these objects and specifically repositories be used by entities? Repository has semantics similar to gateway to external system (DBMS) and using them from entities looks like domain model isolation. However in some cases the only alternative to using repositories is lazy loading which isn’t the best solution.

    • http://enterprisecraftsmanship.com/ Vladimir Khorikov

      Hi Andrey,

      You are right regarding repositories and entities/value objects. Here’s a diagram I like to use to illustrate the idea:
      http://i.imgur.com/NnpYQ65.png

      Entities and value objects reside in the center of the onion, so they can’t refer to the external world, including repositories.

      Without lazy loading, it’s indeed hard to implement that requirement, so it makes sense to adhere to it only when you use a “big ORM” like NHibernate or Entity Framework. Without such an ORM, it’s still feasible in most cases, but will require more work in the app services layer to glue different domain classes together: load related entities manually and pass one to another.