Introduction

Once upon a time, I wrote an article that talked about having separate domain & persistence models while doing Domain-driven design (DDD), and keeping the change tracking functionality of the ORM.

It is by far my most viewed article (thank you all for that). But it came with a gottcha! In order to keep the change tracking functionality we made use of a reconciliation library called Reconciler.

In this article I want to discuss a different approach. One that applies to any kind of ORM, development framework, or programming language.

Goals

  • Build a domain model that handles true business invariants.
  • Have this model be ignorant to persistence concerns.
  • Keep the glorified change tracking functionalty of any ORM that offers it.

Domain Model

I won't go into detail of DDD, as there is already a plethora of sources in the internet, but I still want to touch upon something that I have witnessed reading through articles and OSS projects.

Your domain model is not your data model!

Consider modeling a bank account. This could be an aggregate root, entity, value-object all depending in the context. The business comes along and give us the following requirements:

  • Money can be deposited & withdrawn from an account.
  • An account can never have a negative balance.
  • An account can be frozen, but a reason must be provided for that to happen.
  • One can not deposit or withdraw from a frozen account.

Approach 1 - Aligned with the domain & technicalities

We go ahead and create a class named BankAccount with some properties describing it. It is universally known that we as developers, always know better than the business, and we decide to jam it with properties and methods.

public class BankAccount
{
public Guid Id { get; }
public string Name { get; private set; }
public Status Status { get; }
public decimal Balance { get; private set; }

public BankAccount(
Guid id, string name, Status status, decimal balance)
{
ThrowIfNoName(name);
ThrowIfNegativeBalance(balance);

Id = id;
Name = name;
Status = status;
Balance = balance;
}

public void Withdraw(decimal amount)
{
ThrowIfFrozen("Can not withdraw balance if account is frozen.");

decimal _amount = Math.Abs(amount);
ThrowIfNegativeBalance(Balance - _amount);

Balance -= _amount;
}

public void Deposit(decimal amount)
{
ThrowIfFrozen("Can not deposit balance if account is frozen.");
Balance += Math.Abs(amount);
}

public void ChangeName(string name)
{
ThrowIfNoName(name);
Name = name;
}


private void ThrowIfNoName(string name)
{
if (string.IsNullOrEmpty(name))
throw new BusinessException(
"Bank account name can not be empty");
}

private void ThrowIfNegativeBalance(decimal balance)
{
if (balance < 0)
throw new BusinessException(
"A bank account with negative balance is not allowed");
}

private void ThrowIfFrozen(string message)
{
if (Status.Frozen)
throw new BusinessException(message);
}
}

public class Status
{
public bool Frozen { get; private set; }
public string Reason { get; private set; }
public DateTime? ChangedOn { get; private set; }

public Status(bool frozen, string reason, DateTime? changedOn)
{
if (frozen)
ThrowIfNoReason(reason);

Frozen = frozen;
Reason = reason;
ChangeOn = changedOn;
}

public void Freeze(string reason)
{
ThrowIfNoReason(reason);

Frozen = true;
Reason = reason;
ChangeOn = DateTime.UtcNow;
}

public void Unfreeze()
{
Frozen = false;
Reason = string.Empty;
ChangeOn = DateTime.UtcNow;
}


private void ThrowIfNoReason(string reason)
{
if (string.IsNullOrEmpty(reason))
throw new BusinessException(
"A reason must exist for a frozen account");
}
}

Domain guy: "Id", "Name", "ChangedOn" who asked for all of these?

Technical guy: Well, the account needs to be uniquely identifiable, we also need to show something to the user, and we need to keep track on when an account gets frozen/unfrozen for auditing reasons.

Domain guy: I guess you are right, but I can't see the forest from the trees!

Don't get me wrong we know that those technical concers are important for the overall software, but aren't for the domain we are trying to model and provide value for.

Approach 2 - Aligned with the domain only

public class BankAccount
{
public Status Status { get; }
public decimal Balance { get; private set; }

public BankAccount(Status status, decimal balance)
{
ThrowIfNegativeBalance(balance);

Status = status;
Balance = balance;
}

public void Withdraw(decimal amount)
{
ThrowIfFrozen("Can not withdraw balance if account is frozen.");

decimal _amount = Math.Abs(amount);
ThrowIfNegativeBalance(Balance - _amount);

Balance -= _amount;
}

public void Deposit(decimal amount)
{
ThrowIfFrozen("Can not deposit balance if account is frozen.");
Balance += Math.Abs(amount);
}


private void ThrowIfNegativeBalance(decimal balance)
{
if (balance < 0)
throw new BusinessException(
"A bank account with negative balance can not exist");
}

private void ThrowIfFrozen(string message)
{
if (Status.Frozen)
throw new BusinessException(message);
}
}

public class Status
{
public bool Frozen { get; private set; }
public string Reason { get; private set; }

public Status(bool frozen, string reason)
{
if (frozen)
ThrowIfNoReason(reason);

Frozen = frozen;
Reason = reason;
}

public void Freeze(string reason)
{
ThrowIfNoReason(reason);

Frozen = true;
Reason = reason;
}

public void Unfreeze()
{
Frozen = false;
Reason = string.Empty;
}


private void ThrowIfNoReason(string reason)
{
if (string.IsNullOrEmpty(reason))
throw new BusinessException(
"A reason must exist for a frozen account");
}
}

Our new model is fully aligned with the domain: Balance, Forzen, Reason. We have removed the technicalites: Id, Name, ChangedOn. We did this because "account must have an Id & Name", "status changes need to be tracked when they happen", are not true invariants.

Dealing with the technicalities

Now that our BankAccount class exhibits behavior, and has state that is aligned with the invariants presented to us by the business, we are getting one step closer to the final goal.

But the unavoidable questions arise:

  • How am I supposed to persist this?
  • How am I supposed to differentiate between accounts?
  • How am I supposed to present something to an account's owner?

The first answer to all of the above questions, is to create a new model of the bank account that deals with those technicalities.

public class BankAccountEntity
{
public Guid Id { get; set; }
public string Name { get; set; }
public decimal Balance { get; set; }
// Status
public bool Frozen { get; set; }
public string Reason { get; set; }
public DateTime? ChangedOn { get; set; }
}

We can deal with the technical properties in the upper levels of our architecture, supposing the domain layer is the centerpiece (which it should be!).

But there is a problem! We now have 2 models of an account, and need to perform mapping & reconcilliation to keep change tracking.

If you are in the .NET ecosystem, and are using EF as you ORM, and plan to continue doing so, then the steps below are not neccssary. I would refer you to my previous article on this topic. In the other hand if you are in a situation other than the previously mentioned one, then continue on reading.

Change Tracking

In order to keep the change tracking functionality, we can leverage a different approach which I have first witnessed being used by Derek Comartin. It is a simple, yet quite powerful approach, which can be summarized into 3 steps:

1) Define the data model.
2) Define the domain model.
3) Inject the data model into the domain model, and let the domain model handle the invariants.

Lets have a look at the implementation below. Notice how we are injecting BankAccountEntity (data model) into BankAccount (domain model). We are performing all the state changes (at least those that matter) from within our domain model.

Notice also that we are not performing any modifications to the technical properties (Id, Name, ChangedOn) of the data model inside our domain model, because they do not represent domain concepts. Those properties can be set and validated outside the domain layer, for example in the application layer.

public class BankAccount
{
public Status Status { get; }

private readonly BankAccountEntity _entity;

public BankAccount(BankAccountEntity entity)
{
ThrowIfNegativeBalance(entity.Balance);

Status = new Status(entity);
_entity = entity;
}

public void Withdraw(decimal amount)
{
ThrowIfFrozen("Can not withdraw balance if account is frozen.");

decimal _amount = Math.Abs(amount);
ThrowIfNegativeBalance(_entity.Balance - _amount);

_entity.Balance -= _amount;
}

public void Deposit(decimal amount)
{
ThrowIfFrozen("Can not deposit balance if account is frozen.");
_entity.Balance += Math.Abs(amount);
}

private void ThrowIfNegativeBalance(decimal balance)
{
if (balance < 0)
throw new Exception(
"A bank account with negative balance can not exist");
}

private void ThrowIfFrozen(string message)
{
if (_entity.Frozen)
throw new Exception(message);
}

public BankAccountEntity GetState()
{
return new BankAccountEntity()
{
Id = _entity.Id,
Name = _entity.Name,
Balance = _entity.Balance,
Frozen = _entity.Frozen,
Reason = _entity.Reason
};
}
}

public class Status
{
private BankAccountEntity _entity;

public Status(BankAccountEntity entity)
{
if (entity.Frozen)
ThrowIfNoReason(entity.Reason);

_entity = entity;
}

public void Freeze(string reason)
{
ThrowIfNoReason(reason);

_entity.Frozen = true;
_entity.Reason = reason;
}

public void Unfreeze()
{
_entity.Frozen = false;
_entity.Reason = string.Empty;
}


private void ThrowIfNoReason(string reason)
{
if (string.IsNullOrEmpty(reason))
throw new Exception(
"A reason must exist for a frozen account");
}
}

We have added an additional method GetState, that returns the state of the aggregate. This is needed to retrive the state because BankAccountEntity is private. We are returning a new entity though! If we were to return the same injected entity, than we could not enforce the invariants for the properties which are of high importance to the business.

In the infrastructure layer (specifically the part that deals with persistence, like the repository implementation) we get a hold of the domain model and perform our persisting actions. All operations are done against the domain model, so we: get, add, update and remove domain models, not data models.

public class BankAccountRepository
{
private readonly DbContext _context;

public BankAccountRepository(DbContext context)
{
_context = context;
}

public BankAccount Get(Guid id)
{
var entity = _context.BankAccounts.FirstOrDefault(x => x.Id == id);
return new BankAccount(entity);
}

public void Add(BankAccount account)
{
BankAccountEntity entity = account.GetState();
_context.BankAccounts.Add(entity);
_context.SaveChanges();
}

public void Update()
{
// All changes made to the injected enitity are
// being tracked by the ORM.
_context.SaveChanges();
}

public void Remove(Guid id)
{
var entity = _context.BankAccounts.FirstOrDefault(x => x.Id == id);
if (entity != null)
{
_context.BankAccounts.Remove(entity);
_context.SaveChanges();
}
}
}

So far it is looking good. BankAccount is protecting the invariants, while BankAccountEntity is carrying data. So we effectively have sepparated the concerns of domain knowledge and persistence, while at the same time keeping change tracking with any kind of ORM.

Hold on a second!!

If we are injecting the data model into the domain model, it means that the domain layer needs to have knowledge about the data model. This only holds true if the data model lives in the same layer/project as the domain model. We can not define the data model in the infrastructure layer, beacuse it introduces a circular dependency between the projects, which gets manifested as a compile-time error in statically defined languages.

The clean architecture enthusiast might have a problem with this!

No way am I going to put my data model in the domain project and pollute it with ORM configurations.

If you are using an ORM that supports configurations than this is not a problem anymore, because you can reference the POCO classes from the infrastructure layer and define your configurations there. But lets just say that your ORM works only with data annotations and you want to avoid including a reference to your ORM package/lib in your domain layer. Lets see how we can solve for this.

Interface Obsession

If you have come this far, than I would recommend to stop here, because your software will not gain much from the rest of this. But if you are curious, and want to go all the way, than be my guest πŸ™‚.

I don't know if this is actually a term that is used, but we are going to referr to interface obsession, as a developer's obsession with using interfaces were not justified.

This is especially true when defining interfaces for POCO classes. This is completely unnecessary when you are going to have only one implementation of the entity (or any object as a matter of fact), but I can see it being "useful" in this case.

Since we want to remove BankAccountEntity from the domain layer, we are going to create an interface for it. IBankAccountState inherits from ICloneable so that we invoke the Clone method in the domain model.

public interface IBankAccountState : ICloneable
{
public Guid Id { get; set; }
public string Name { get; set; }
public decimal Balance { get; set; }
public bool Frozen { get; set; }
public string Reason { get; set; }
public DateTime? ChangedOn { get; set; }
}

Lets change how the BankAccount domain model expose GetState() and now return a clone of the injected data model.

namespace MyProject.Domain
{
public class BankAccount
{
private readonly IBankAccountState _state;

public BankAccount(IBankAccountState state)
{
// Guards are here.
_state = state;
}

public IBankAccountState GetState()
{
return _state.Clone() as IBankAccountState;
}
}
}

We can now define the BankAccountEntity in the infrastructure layer. Note the attributes (Table, Key, Required, MaxLength) aren't a concern of the domain anymore.

Since BankAccountEntity implements IBankAccountState, it also has to implement IClonable.

namespace MyProject.Infrastructure
{
[Table("BankAccount")]
public class BankAccountEntity : IBankAccountState
{
[Key]
public Guid Id { get; set; }

[Required]
[MaxLength(100)]
public string Name { get; set; }

public decimal Balance { get; set; }
public bool Frozen { get; set; }

[MaxLength(100)]
public string Reason { get; set; }

public DateTime? ChangedOn { get; set; }

public object Clone()
{
return new BankAccountEntity()
{
Id = Id,
Name = Name,
Balance = Balance,
Frozen = Frozen,
Reason = Reason,
ChangedOn = ChangedOn
};
}
}
}

While in the repository nothing has changed. GetState returns an IBankAccountState but since BankAccountEntity implements it, we can add it to the DbContext without any issues.

namespace MyProject.Infrastructure
{
public class BankAccountRepository
{
...

public void Add(BankAccount account)
{
IBankAccountState entity = account.GetState();

_context.BankAccounts.Add(entity);
_context.SaveChanges();
}

...
}
}

Being Consistent (Optional)

We established everything that we set to, but as you can imagine, a codebase is not build from a single domain model, but a lot of them aggregated into different roots. With a little tweak we can make this flow more consistent.

Lets define a base class that all aggregate roots inherit from.

public abstract class AggregateRoot<T> where T : class, ICloneable
{
protected readonly T _state;

public AggregateRoot(T state)
{
if (state == null)
throw new ArgumentNullException(nameof(state));

_state = state;
}

public T GetState()
{
return _state.Clone() as T;
}
}

We add the GetState method and return the generic parameter T, which is constrained to be a type that implements ICloneable and is a class, so that every aggregate root has the ability to return a cloned version of the state that it encapsulates.

namespace MyProject.Domain
{
public class BankAccount : AggregateRoot<IBankAccountState>
{
public BankAccount(IBankAccountState state)
: base(state)
{
ThrowIfNegativeBalance(state.Balance);
}
}
}

One might say that the GetState method is a persistence concern, but I would argue against it. There is nothing that says the domain has knowledge about peristence. The domain model simply returns a copy of its state, what happens next is none of its business.

Summary

In this article, which is a revisit of the previous one on this topic. We explored a new way to deal with domain & data model sepparation, while keeping change tracking.

  • We built a domain model that handles true business invariants.
  • Removed all technicalities and persistence concerns from it.
  • Kept the change tracking functionalty.
  • Sepparated the data models and all ORM configurations into the infrastructure layer with the help interfaces.

Let there be creativity in your domain model 😁.


If you found this article helpful please give it a share in your favorite forums πŸ˜‰.
The solution project is available on GitHub.