Onion architectures made simple

Here is an attempt to understand what onion architectures are. The code below should make possible to reveal and answer some common  problems, including consistency. Onion architectures promote the fact that business code should be able to ignore all issues related to storage and, more generally, infrastructure. The technical components are referenced in the class that does the bootstrapping of the business, so that the business code in question does not reference any technical assemblies. The business code represents the Bounded Context of the (BC). It is possible to inject persistence code in the BC, through delegates, but this implies that the business code knows these elements somehow, even if they are subject to strong abstractions. This is unfortunate, even if it is acceptable.

To begin, we will recall that DDD notifications are key elements of the BC. They are not second class citizens, they contain business, valuable information, and they represent much more than raw . It is therefore necessary to persist them, because they are totally part of the BC. On the other hand, we have entities, services, and all sorts of artifacts that all together incarnate the model. This model must also be persisted, imperatively. Notifications and entities should be able to persist in different engines. If the business model is persisted in a RDBMS, it is likely that this storage format is not suitable for notifications. In addition, the technical components involved in the persistence of a model on one hand, and in the emission of notifications on the other hand, are components that share little. Therefore, a first challenge is to ensure the consistency of these different elements. A problem of persistence of the entities must never give rise to false notifications, and vice versa, it is necessary to be able to throw again all notifications if the service bus fails when the model has been persisted. These 2 purposes must imperatively evolve together, because both are the warrants of the system’s consistency.

In the following code, we identified 3 elements of infrastructure : databases, REST APIs, and the Enterprise Service Bus (ESB). Databases are meant to persist the model and the notifications, REST APIs are meant to retrieve some kind of data (let’s say reference data…), and the ESB is meant to convey notifications. These elements should be declared in the application service layer, upstream to the BC. In the following example, REST APIs are set as “ExternalDataSources”, databases as “InternalStorage”, and ESB as “ServiceBus”.

new BootStrapper()
    .SetExternalDataSources()
    .SetInternalStorage()
    .SetServiceBus()
    .Execute(d => d.Process());

The core business process is handled by the d => d.Process() lambda expression and is invoked later in the BC, once the technical environment of the application layer has been defined.

How does this code work?

As we do not attempt to create a generic framework, the Execute method takes as argument an expression that consumes an instance of the BC. No interface here !

No infrastructure element is introduced in the BC. If delegates or abstractions were injected for infrastructure purposes, there would be a risk of introducing very complex subtleties into the life cycle of entities in the model, for example by allowing partial updates of data at different stages of the process. Here, we are sure to separate the behavior of the rest, and to work on stable states over time. And at the end of processing, make a snapshot of the condition of the model in order to persist it, once the danger zone is passed. When the BC is asked to perform its processing, it introduces no notion related to the infrastructure.

This code makes a very weak distinction between entities and notifications. Each of these elements goes back from the BC into the application service after treatment, for being persisted. Why ? We simply just want to be able to manage these kinds of elements, regarding infrastructure. If domain events were released directly from the business code, we should encounter some risks of releasing wrong events, that may need to be invalidated (after their release) by all kinds of errors that may raise afterwards, bringing us total inconsistency between domain BCs, as the internal state of the current may be broken too. We want to be able to manage some kind of transaction between the state of the model, its persistence, and the release of its events. If distributed transactions are almost impossible, it is almost possible to guarantee a relative consistency, by orchestrating it.

The domain BC inherits from the class DomainBase, that exposes 2 collections. One for entities, one for events.

public class DomainBase<TEntities, TEvents>
{
    private readonly List<TEntities> _entities;
    private readonly List<TEvents> _events;

    public ReadOnlyCollection<TEntities> Entities => _entities.AsReadOnly();
    public ReadOnlyCollection<TEvents> Events => _events.AsReadOnly();

    protected DomainBase()
    {
        _entities = new List<TEntities>();
        _events = new List<TEvents>();
    }
}

Once the business process has completed, the application service grabs items from theses collections, in order to persist them. Then, the first step is to persist entities. This operation guarantees the state of the BC to be consistent. Therefore, we persist events. At this stage of the process, we can’t be sure events will correctly be released, so we need to persist them, in order to release them later (what happens if the service bus is unavailable… ?). Events are managed as entities, in a dedicated storage structure. Once everything is persisted, let’s release events !

This workflow is not perfect… If it is impossible to persist events, they can be lost. The current use-case covers two storage engines, but maybe should we have one collection that carries entities and events, and one unique storage engine. This could help to manage consistency.

public void Execute(Func<BoundedContext, bool> func)
{
    // Breaks if any error occurs
    try
    {
        // Starts the process
        func.Invoke(_boundedContext);

        // Creates a TransactionScope (if possible, more stuff is needed depending on storage runtimes...)
        // Transaction should be the responsability of each context
        using (var transaction = new TransactionScope())
        {
            if (_entitiesContext.Persist(_boundedContext) && _eventsContext.Persist(_boundedContext))
            {
                transaction.Complete();
            }
        }

        // If everything is OK, returns to the database and play persisted domain events...
        // TODO : Separate Storage from Events
        _serviceBus
            .WithAcknowledgement()
            .Send(_boundedContext);
    }
    catch (Exception exception)
    {
        System.Console.WriteLine(exception);
    }
}

Events are persisted, then released. To allow the application service to “remember” the events that need to be released, identities should be defined on the client side, not on the database side. This is why events and entities all inherits from the class DomainObjectBase :

public class DomainObjectBase
{
    private readonly Guid _identity;
    // ReSharper disable once ConvertToAutoProperty
    public Guid Identity => _identity;

    protected DomainObjectBase()
    {
        _identity = Guid.NewGuid();
    }
}

This article is a bit long. There are many things to discuss. I propose you to share some other thoughts on a second page, here.

Add a Comment

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

62 + = 65