Insight into an entity

One of the challenges of domain driven design is to keep the domain model simple. Entities form the core of a domain model. As the application becomes useful the domain model becomes complex. The entity classes become large in size, having a lot of fields and methods. More and more fields are added to the entity because thats where they belong. As more fields get added one has to add methods to the entity class following the tell-don't-ask principle. With decoupled domain model one can remove the responsibility of construction of other data representations from the entity hence reducing the number of methods. Also, the use of extract class refactoring allows one to create smaller classes. This creation of smaller classes requires deeper understanding of the domain and the entity under question. We would see in this chapter than with better understanding of the entity the class size (and cognitive weight imposed by it) can be reduced even before applying the extract class refactoring approach.

Let's dig a bit deeper in the entity. On the surface an entity class is made up of primitives, other entities and children entities. This level understanding of the structure of a entity doesn't provide useful answers, so lets categorize these field types. This classification uses functional and life-cycle behavior.

Taxonomy of an entity

Functional

  • Infrastructure identification is the identity of the entity from persistence point of view. There is no reason for any other domain object to know about it. The surrogate primary key in database is an example of this.
  • Business identification of entity may also have field(s) for identification of object from business perspective. This is used by the application's users to identify the entity in the real world. Typically this number is important to the users of the application.
  • Business attributes are important from the business perspective. These form the core domain and the business logic requires them. This data is owned by the entity and gets deleted when the entity is deleted. These can be primitives as well as other entities. There are two type of these attributes, internal and encapsulated.
    • Internal business attributes are created to facilitate business behavior expected from an entity but is not directly visible to the users of the system. Only the outcome of services provided by these attributes are visible.
    • Encapsulated business attributes are visible to the user in some direct or slightly altered form.
  • Extrinsic attributes do not play any role in core business. They are important for the user, presentation, of the application but their value does not affect the business logic. [Application validation and database invariance would not qualify as business logic. Hence duplicate and mandatory checks are not included in core business logic, but business validations are. Their life cycle are tied to the entity like business attributes.]
  • Business associations are the entities which should exist prior to creation of this entity. They provide business context for an entity. These are not owned by the entity from the life cycle perspective but they too are also part of the core domain and play a role in business logic.
  • Extrinsic associations are similar to business associations except that they do not play a role in business logic.
  • Infrastructure attributes do not affect the business logic but do affect how infrastructure layer uses the entity. Version field in the entity used for optimistic offline lock is an example of it.
  • Extrinsic infrastructure attributes are infrastructure related data which the user of application doesn't care about, neither does the behavior of infrastructure layer determined by its value. These fields are important to the system administrators who are responsible for running the application. Auditing related fields are such meta data.

Lets look at our Loan entity as an example.

class Loan {
    //Infrastructure identification
    long id;
    //Infrastructure attributes
    long version;
    //Extrinsic infrastructure attributes
    long createdByUserId;
    long lastModifiedByUserId;
    DateTime creationTime;
    DateTime modificationTime;
    //Business identification
    String loanNumber;
    //Business attributes
    Money amount;
    double interestRate;
    double numberOfInstallments;
    LocalDate startDate;
    LoanFees fees;
    InstallmentPaymentSchedule schedule;
    //Extrinsic attributes
    String purposeOfLoan;
    String notes;
    
    //Business associations
    LoanProduct product;
    Customer customer;
    //Extrinsic associations
    Fund fund;
    LoanProduct product;
}

One of the core ideas of domain driven design is to isolate the domain model from other programming concerns like presentation, persistence, remote interaction etc. The loan entity definition would do that. The source of fund, purpose of loan, etc are not important to us because loan behavior is affected by them in our application. In another application this might not be the case, if for example, only certain percentage of a fund can be utilized for a certain kind of loan. In other words they are domain specific. The infrastructure fields might be useful to us from auditing purpose. The reason it is categorized as infrastructure is because they apply to most entities in the application. Typically such concerns are handled centrally and are orthogonal to domain concerns. So if we look closely it is the business attributes and associations that we really care about, when we are in the domain layer. Hence our loan class can be structured as:

class Loan extends ExtrinsicLoanData {

    //Business attributes
    Money amount;
    double interestRate;
    double numberOfInstallments;
    LocalDate startDate;
    LoanFees fees;
    InstallmentPaymentSchedule schedule;
    
    //Business associations
    Customer customer;
}
class ExtrinsicLoan extends Entity {
    String loanNumber;
    Fund fund;
    String purposeOfLoan;
    String notes;
    String loanNumber;
    LoanProduct product;
}
class Entity {
    long id;
    long version;
    long createdByUserId;
    long lastModifiedByUserId;
    DateTime creationTime;
    DateTime modificationTime;
}

We have created a smaller Loan class in terms of its responsibilities. There is nothing more to sub-classing here than separation of responsibilities in separate files and one can use partial classes in C#. It is important to stress that such separation should not be done arbitrarily to make the classes look small. Even though deep class hierarchy is not desirable the fact that one call give it name which illustrates the nature of class is better than partial class. In the rest of the chapter we would learn more about how this insight helps in programming difficulties.

The extrinsic fields might not play a role in business logic in the application but might be useful for reporting purposes. Such reporting kind of queries where extrinsic fields are used to filter and join data, has business value but no business logic.

Requirement

An entity can also be analyzed on another axis. Entity by definition have a life cycle existing in multiple functional states during this. In each of these states certain fields in entity are mandatory and others are optional. The functional classification done above is orthogonal to this, e.g. a extrinsic field can be mandatory or optional. The fields can be mandatory or optional based on state of the entity. (In our application loan can be in any of these states, New, Approved, Active, Risky and Closed.)

Visibility

Typically most data on entity is visible via the service layer to the external world. There are some fields in entity which are used for internal calculations only and need not be exposed outside. We can categorize them as internal and external fields.

Along with the characteristics of the fields on an entity the state of entity is also useful for understanding an entity in isolation from rest of the domain. An entity can be in multiple states in its lifecycle. Our loan entity exists in: New, Approved, Disbursed, Risk and Closed states. An new entity can be only in certain states depending on the domain. The rest of the states are arrived at by making state transitions in rather than directly jumping onto them. For example in our application a loan cannot be in a Risk state directly.

Entity Construction

Entity construction is different from object construction. With entity construction we are interested in creating a representation of domain concept. Object construction is one step in this process. With that differentiation in mind, lets look at entity construction which happens in following context. (when not using decoupled domain model the entities can be constructed in presentation/application layers as well)

  1. Entity activation: An entity is activated when repository retrieves it from the underlying data source.
  2. New entity: An object for a new entity is constructed and populated.
  3. Entity under test: A unit test for the entity class creates one.
  4. Dependent class under test: A unit test for a service or another entity creates this entity for setup.

Entity activation doesn't require any design choice. In a simple case, the columns from database row are mapped to the entity object. Modern OR mapping tool can work with private constructor and fields to achieve this.

Before we dive into other scenarios lets look at what options we have. We have two mechanism constructor and methods. There are two extremes here which we can employ. A constructor which takes all the field or setters for every field. Both of these are also quite common and they also come with their own set of issues.

Constructor takes all

public Loan(Money amount, double interestRate, double numberOfInstallments, LoanFees fees, LocalDate disbursementDate, LoanProduct product, Customer customer, String loanNumber, Fund fund, String purposeOfLoan, String notes) {
    if (amount 
}
Loan loan = new Loan(amount, double interestRate, double numberOfInstallments, fees, null, LoanProduct product, Customer customer, null, fund, purposeOfLoan, notes)

This ensures the object cannot be constructed in an invalid state. But if suffers from following weaknesses.

  • we have to pass null for certain fields.
  • any new construction of few entities like this can discourage someone from writing tests.
  • every new addition of a field to loan would result in changing all the callers to pass null or some default value to it. While modern IDEs support change-method-signature refactoring, the readability of code suffers and you have a lot of modifications in different files which need merging. Imagine merging when two people added different parameters to the same constructor at the same time.
  • the constructor doesn't tell me which parameters can be null which cannot be. I learn that by running my code or by reading the constructor code.

Set-every-field

public Loan() {
}
public Loan amount(Money amount) {
    this.amount = amount;
    return this;
}
public Loan interestRate(double rate) {
    this.interestRate = rate;
    return this;
}
//so on....
Loan loan = new Loan().amount(amount).interestRate(rate)....;

The provides complete ease and control in constructing an entity, but has following issues.

  • it doesn't ensure whether the entity is in a valid state.
  • business constraints which depend on more than one field cannot be enforced. This is one of the values of using a domain model.

Each of these two extremes have significant downsides. If constructing an entity is difficult it would discourage one from writing tests. When tests become difficult to write some might stop writing them. A test which is cluttered with non-relevant details is hard to understand and maintain as well. These problem get elevated for tests like Application Service Test, which involves constructing multiple entities to setup the context for the test. On the other extreme, an entity which is constructed by setting all the object's fields via setter method creates design issues. The presence of setters encourages them to be used in non-construction context. This (coupled with getters) is a slide towards anemic domain model.

Our insight into taxonomy of an entity can help break this deadlock for large entities, helping us find the right balance. Before we look at how, let see some implications of this insight.

  1. Only mandatory attributes are required to construct an entity from the domain perspective as by definition other attributes are optional.
  2. An entity is in useful state for testing even when the extrinsic fields are not set since these do not have any bearing on the domain logic which requires testing.
  3. System attributes are not relevant when testing for functional behavior. In a well built domain layer the system logic although dependent on the system attributes they are should built in the infrastructure layer. This lends to clear decoupling of domain logic and system logic.
  4. Internal business attributes are hidden from client constructing an entity
  5. Non-mandatory business attributes come into existence based on some state change. This is more of a useful approach to deconstruct an entity's state. It is quite likely that an entity is also constructed with non-mandatory business attributes but it can also be seen as combination of, construction with basic mandatory attributes followed by state change.

Based on these outcomes we can pragmatically reduce the entity construction problem which benefits from domain understanding as well as good object design principles. Lets see some code at last to see what all this means in the different context we have outlined earlier.

public Loan(Money amount, double interestRate, double numberOfInstallments, LoanFees fees, LoanProduct loanProduct, Customer customer) {
}
Loan loan = new Loan(amount, interestRate, numberOfInstallments, fees, customer);
loan.purpose(purpose).fund(fund).notes(notes).product(product)...;
//state transitions
loan.approve(approvedBy);
loan.disburse(disbursementDate);

We have taken three decisions here.

  1. The entity constructor takes only mandatory business attributes and associations required in that state. The attributes/associations which are extrinsic or not mandatory in the first state are not used. Since the extrinsic fields don't play any role in business logic the entity doesn't demand their presence even if they are mandatory. Failing to set them would cause persistence to fail which would be indicated by database constraints. The reason being the entities constructors are invoked far often for unit test, which are interested in logic and not mapping.
  2. All other fields of the entity are set using setters. Most likely these fields would have getter as well. Doing so doesn't make the our domain model anemic as these do not have any role to play in behavior of the entity. LoanProduct is extrinsic only in the context of Loan not for the entire domain model while fund, notes are globally extrinsic. You can choose to not make this distinction and consider LoanProduct as business association instead. It is important to note that we do not have any setters for business attributes or associations which ensures that the entity cannot be in an invalid state.
  3. The other mandatory business attributes are provided via state transition methods. This helps in keeping the constructor small and mirroring the domain.