Architecture – Domain Model Design – Best Practices for Robust Fluency, Encapsulation, and Extensibility

Architecturedesign-patternsdomain-driven-designdomain-model

I am striving to have the most maintainable architecture possible in my application, but I cannot decide on a proper domain model design.

Here is an example domain model for a user:

public class User // Referred to as the domain model
{
    // A model whose properties map exactly to a database table schema 
    private UserDataModel Model { get; set; } // Posts data back to the database

    public string FirstName
    {
        get
        {
            return Model.FirstName;
        }
        set
        {
            // Validation & Observer logic here
            Model.FirstName = value;
        }
    }
    public string LastName
    {
        get
        {
            return Model.LastName;
        }
        set
        {
            // Validation & Observer logic here
            Model.LastName = value;
        }
    }
    public string FullName
    {
        return $”{FirstName} {LastName}”;
    }
    public Company Company // Other domain model with a similar design
    {
        get
        {
            return Model.Company;
        }
        set
        {
            // Validation & Observer logic here
            Model.Company = value;
        }
    }

    // List of other domain models with similar designs
    public IList<Order> Orders { get; set; } 
    // …

    internal User(UserDataModel user)
    {
        Model = user;
    }

    // Domain specific methods here 
    //(e.g. IsAdmin, CalculateBill, SendPasswordResetEmail, CanPlaceOrder, etc.)
}

Essentially, my concerns relate to what are the best practices for having an effective, efficient, and maintainable domain design that allows for robust consumption, strong encapsulation, and high extensibility / maintainability or scalability. Specific questions are:

  1. Properties

    a) Should my domain model contain properties similar to the underlying data model or properties that are only relevant to the application’s consumption of the domain model (i.e. properties like FullName, boolean flags like Dirty (updates occurred), Prototype (i.e, brand new dummy model for creation by client/consumer))?

    b) Should the underlying data model be exposed publicly for access or should it only be accessible via methods? Should only parts of the data model be returned or the entire object?

    c) Should the domain model include linked objects (i.e like the Company object in the example) or just the relevant value to that linked object (i.e. the Name property of the Company object instead of the entire object)?

    d) Should setters be similar to above or only via methods called on the domain model or private methods called in the property setter (i.e. set { SetFirstName(value); } )? (Note: The setters will implement validation engine calls & observer calls & service calls, so they will be relatively chunky)

  2. Construction

    a) Should I approach construction of a new domain model from the client/consumer via several set methods or purely mapping the properties at the client/consumer level or via a factory / builder pattern?

    b) Should my domain model completely hide the underlying data structure it represents (i.e. appears as if it is a object that was never part of a database)?

    c) Should validation be done at construction level (i.e. as properties are set) or left up to the client/consumer calling an “IsValid” method on the domain model when appropriate?

  3. Validation

    a) Should validation errors be stored in a collection in the domain model or in a separate errors model / singleton class?

    b) Should validation be done at the domain model level or data model level?

  4. Retrieval

    a) For lists of objects (i.e. orders), should the list be private and access via a “GetOrder(string orderNo)” method? Wouldn’t this be a violation of the Single Responsibility principle (I.e. the method is a pseudo repository / factory) or no?

    b) How should retrieving an entire list be done without affecting the containing domain model or the domain models in the list? Should a clone be used despite it being potentially memory intensive (a user could have thousands of orders with their own linked domain models)?

I am ideally trying to avoid developing an unmaintainable domain design that is a pain to consume or test. I want the domain model to be as fluent & robust as possible thus allowing it to be read like prose to the client/consumer and representing the actual domain not the underlying data structures (i.e. it should be able to act as if it standing on its own without a backend).

Best Answer

Frankly, glue classes that merely pass through fields from the data model don't really add significant value to your application, nor are they particularly interesting from a functional perspective.

So before we get too deeply into your individual questions, let me propose a simple foundational architecture:

DB <---> ORM <---> SL/BLL <---> VM <---> V

The DB is your database. The ORM is your Object-Relational Mapper; it communicates with the database via SQL, and exposes CRUD methods. The SL/BLL is your Service Layer/Business Logic Layer; it converts business operations such as CreateInvoice into CRUD methods for consumption by the ORM. The VM is the ViewModel; it coordinates UI interaction with the SL/BLL. The V is the View; it contains surface-level UI interaction and validation code-behind.

Your sample code is suggestive of C#. In C#, your ORM (very likely Entity Framework) can produce code-generated entity classes that can be glued to domain logic using the partial keyword, allowing you to write custom domain and validation logic for each entity class without the pain of creating all that boilerplate for DTO's.

A lean architecture like this one should allow you to develop an easily maintainable application with the minimum necessary boilerplate code. Unless you're building an elaborate, crystal-cathedral architecture in Java for a large development team, this representative architecture should be all of the abstraction you will ever need.