Designer's commitments

Before going a bit deeper into the low-level stuff, here are some key sentences we should better often refer to:

  1. I shall collaborate with domain experts;
  2. I shall focus on the ubiquitous language;
  3. I shall not care about technical stuff or framework, but about modeling the Domain;
  4. I shall make the implicit explicit;
  5. I shall use end-user scenarios to get real and concrete;
  6. I shall not be afraid of defining one model per context;
  7. I shall focus on my Core Domain;
  8. I shall let my Domain code uncoupled to any external influence;
  9. I shall separate values and time in state;
  10. I shall reduce statefulness to the only necessary;
  11. I shall always adapt my model as soon as possible, once it appears inadequate.

As a consequence, you will find in mORMot no magic powder to build your DDD, but all the tools you need to focus on your business, without loosing time in re-inventing the wheel, or fixing technical details.

Defining objects in Delphi

How to implement all those DDD concepts in an object-oriented language like Delphi?
Let's go back to the basics. Objects are defined by a state, a behavior and an identity. A factory helps creating objects with the same state and behavior.

In Delphi and most Object-Oriented languages (OOP - including C# or Java) each class instance (always inheriting from TObject):

  1. State is defined by all its property / member values;
  2. Behavior are defined by all its methods;
  3. Identity is defined by reference, i.e. a=b is true only if a and b refers to the same object;
  4. Factory is in fact the class type definition itself, which will force each instance to have the same members and methods.

In Delphi, the record type (and deprecated object type for older versions of the compiler) has an alternative behavior:

  1. State is also defined by all its property / member values;
  2. Behavior are also defined by all its methods;
  3. But identity is defined by content, i.e. RecordEquals(a,b) is true only if a and b have the same exact property values;
  4. Factory is in fact the record / object type definition itself, which will force each instance to have the same members and methods.

We propose to use either one of the two kinds of object types, depending on the behavior expected by DDD patterns.

Defining DDD objects in mORMot

DDD's Value Objects are probably meant to be defined as record, with methods (i.e. in this case as object for older versions of Delphi). You may also use TComponent or TSQLRecord classes, ensuring the published properties do not have setters but just read F... definition, to make them read-only, and, at the same time, directly serializable.
If you use record / object types, you may need to customize the JSON serialization when targeting AJAX clients (by default, records are serialized as binary + Base64 encoding, but you can define easily the record serialization e.g. from text). Note that since record / object defines in Delphi by-value types (whereas class defines by-reference types - see previous paragraph), they are probably the cleanest way of defining Value Objects.

DDD's Entity objects could be either regular Delphi classes, or inherit from TSQLRecord:

  • Using PODOs (Plain Old Delphi Object - see so-called POJO or POCO for Java or C#) has some advantages. Since your domain has to be uncoupled from the rest of your code, using plain class helps keeping your code clean and maintainable.
  • Inheriting from TSQLRecord will give it access to a whole set of methods supplied by mORMot. It will implement the "Layer Supertype" pattern, as explained by Martin Fowler.

DDD's Aggregates may either benefit of using mORMot's 3, or you can use a repository service.

  • In the first case, your aggregate roots will be defined as TSQLRecord, and you will benefit of all CRUD methods made available by the framework;
  • Otherwise, you should define a dedicated persistence service, then use plain DTO (like Delphi record) or even publish the TSQLRecord types, and benefit of their automated serialization.

In all cases, when defining domain objects, we should always make the implicit explicit, i.e. defining one type (either record/object or class) per reality in the model.
Thanks to Delphi's strong typing, it will ensure that the Domain Ubiquitous language will appear in the code.

DDD's DTO may also be defined as record, and directly serialized as JSON via text-based serialization. Don't be afraid of writing some translation layers between TSQLRecord and DTO records or, more generally, between your Application layer and your Presentation layer. It will be very fast, on the server side. If your service interfaces are cleaner, do not hesitate. But if it tends to enforce you writing a lot of wrapping code, forget about it, and expose your Value Objects or even your Entities, as stated above. Or automate the wrapper coding, using RTTI and code generators. You have to weight the PROs and the CONs, like always...

DDD's Events should be defined also as record, just like regular DTOs. Note that in the close future, it is planned that mORMot will allow such events to be defined as interface, in a KISS implementation.

mORMot's BATCH support is a convenient implementation of the Unit of Work pattern (i.e. regrouping all update / delete / insert operations in a single stream, with global Commit and Rollback methods). Note that the current implementation of Batch* methods in mORMot, which focuses on Client side, should be enhanced to be more convenient and available on the server side, i.e. in the Application Layer.

Defining services

In practice, mORMot's Client-Server architecture may be used as such:

  • Services via methods can be used to publish methods corresponding to your aggregate roots defined as TSQLRecord.
    This will make it pretty RESTful compatible.
  • Services via interfaces can be used to publish all your processes.
    Dedicated factories can be used on both Client and Server side, to define your repositories and/or domain operations.

Both methods will allow proper customization, and, especially for the second, offer both integrated and automated process, e.g. RESTful access, JSON marshalling, session, security, logging, multi-threading.

Building a Clean architecture

A common DDD architecture is expressed as in the following model, which may look like a regular multi-Tier design at first, but should be implemented as a Clean Architecture.

Layer Description
Presentation MVC UI generation and reporting
Application Services and high-level adapters
Domain Model Where business logic remains
Data persistence ORM and external services
Cross-Cutting Horizontal aspects shared by other layers

Physically, it involves a common n-Tier representation splitting the classical Logic Tier into two layers, i.e. Application layer and Domain Model layer. At logical level, DDD will try to uncouple the Domain Model layer from other layers, so the code itself will rely on interfaces and dependency injection to let the core Domain focus on the business logic, not on implementation details (e.g. persistence or communication).

This is what we called a Clean Architecture, defined as such:

Clean Uncoupled Domain-Oriented Architecture

The RESTful SOA components of our Synopse mORMot framework can therefore define such an Architecture:

Clean Domain-Oriented Architecture of mORMot

As we already stated, the main point of this Clean Architecture is to control coupling, and isolate the Domain core from the outer layers. In Delphi, unit dependencies (as displayed e.g. by our SynProject tool) will be a good testimony of proper objects uncoupling: in the units defining your domain, you may split it between Domain Model and Domain Services (the 2nd using the first, and not vice-versa), and you should never have any dependency to a particular DB unit, just to the framework's core units, i.e. SynCommons.pas and mORMot.pas. Inversion of Control - via interface-based services or at ORM initialization level - will ensure that your code is uncoupled from any low-level technical dependency. It will also allow proper testing of your application workflows, e.g. stubbing the database if necessary.

In fact, since SOA tends to ensure that services comprise unassociated, loosely coupled units of functionality that have no calls to each other embedded in them, we may define two levels of services, implemented by two interface factories, using their own hosting and communication:

  • One set of services at Application layer, to define the uncoupled contracts available from Client applications;
  • One set of services at Domain Model layer, which will allow all involved domains to communicate with each other, without exposing it to the remote clients.

In order to provide the better scaling of the server side, cache can be easily implemented at every level, and hosting can be tuned in order to provide the best response time possible: one central server, several dedicated servers for application, domain and persistence layers...

Due to the SOLID design of mORMot you can use as many Client-Server services layers as needed in the same architecture (i.e. a Server can be a Client of other processes), in order to fit your project needs, and let it evolve from the simplest architecture to a full scalable Domain-Driven design.