7. Modeling Domain Concepts

Many projects could benefit from carefully crafted domain models, but don’t. Oftentimes the lack of thoughtful modeling is due to the concepts of a business being perceived as data. After all, we are constantly reminded that “data is the most important asset of the business.” Given the strong affinity for big and fast-moving data, it might seem difficult to argue against this reasoning. But even if you assign the highest importance to data, that data means nothing without smart people who learn how to process it to the point of extracting the greatest possible value from it.

This is where tactical modeling comes in. Once you have a fairly good understanding of the strategic direction to be taken, more focus can be given to the implementation. When the implementation of the business model is primarily based on data, the outcome is mostly composed of the following elements:

Big nouns are the modules. When there’s a data-centric focus, the modules generally take on characteristics of data manipulation tools rather than business drivers: factories, entities, data access objects or “repositories,” data transfer objects, data services, and data mappers.

Medium-sized nouns are entities, business-centric services, and data manipulation tools. These were all listed in the previous point.

Small nouns are the fields, attributes, or properties of the entities or other data-centric tools. Think of the detailed parts of any data object—those are small nouns.

During their training and consulting engagements, the authors enjoy asking others to describe their day from the time they awoke until the present time of that same day, but only with the use of nouns. Although perhaps expecting to hear several in the group speak up and rattle off “alarm, bathroom, closet, clothes, kitchen,” and the like, it might be surprising that most have difficulty even getting started. That’s because people don’t think in nouns.

Instead, people think in concepts that, when communicated to other people, must be described by means of expressiveness. To do so, they need word forms and figures of speech that include nouns, but go far beyond them. When software is implemented without people making these efforts, it leaves software in a state of confusion. Each reader of source code must interpret the meaning from hundreds or thousands of big, medium-sized, and small nouns. This approach is so complex and fraught with peril that projects regularly fail as a result. It is simply impossible to keep in mind every single noun, or even a small number of them, and their impacts on all the many others. Clarity, narrow meaning, and clear behavioral intentions are vital.

This chapter highlights the importance of modeling business concepts using rich expressions, and augmenting the data and business rules with language that conveys the best ways for the business work that is done with the software to be carried out. Doing so will convey explicit meaning. Other guidance is available on these topics, such as the books Implementing Domain-Driven Design [IDDD] and the quick-start guide Domain-Driven Design Distilled [DDD-Distilled]. Here we consider the tools in less detail, but with enough information provided to understand the concepts.

Entities

An entity is used when modeling a conceptual whole that is an individual thing. Giving an entity individuality is done through generating and assigning it a unique identity. Its uniqueness must be achieved within the scope of the entity’s life cycle. If the entity is considered a top-level concept within a modeling context, it must be assigned a globally unique identity. If an entity is held inside a parent entity, the child must be uniquely identified only within its parent. Note that it’s the combination of the entity’s module name, concept name, and generated identity that collectively make a given entity instance unique.

In the following, two entities are both assigned identity value 1, but they are globally unique because of their different module names and concept names:

com.nucoverage.underwriting.model.application.Application : 1
com.nucoverage.rate.model.rate.PremiumRate : 1

An entity can also be mutable (modifiable), and an object-based entity is very likely to be mutable, but that alone does not qualify it as an entity. That’s because an entity can be immutable instead. Its uniqueness is the key to an entity being an entity, an individual thing in the model. In Figure 7.1, the Application is an entity and applicationId holds its unique identity.

Image

Figure 7.1 An Entity and Value Object that compose an Aggregate as a transactional boundary.

Value Objects

A Value Object is a modeled concept with one or more data attributes or properties that together compose a whole value; thus, it is a constant, an immutable state. Unlike an Entity, which has a unique identity, a Value Object’s identity is not meant to be unique. It can be said that a value has no identity, but it does have a concept of identity. A value’s identity is determined by its type name and all of its composed attributes/properties together; that is, its full state identifies each value. In the case of values, uniqueness of identity is not expected. Two values with the same identity are equal. Whether or not value identity makes sense to you, what’s most important to understand is that equality between two or more values is quite common.

An example of a value is the integer 1. Two integer values of 1 are equal, and there can be many, many values of 1 in a single software process. Another example value is the text string "one". In some modern programming languages, the integer value 1 is a scalar, one of the basic single-value types. Other scalars include long, boolean, and char/character. Depending on the source of its definition, a string might or might not be considered a scalar. Yet, that’s not important: A string is generally modeled as an array of char and its functional behavior provides useful, side-effect-free operations.

Other value types in Figure 7.1 include ApplicationId (indicated by the applicationId instance variable) and Progress. Progress tracks the progression of workflow processing steps carried out on the parent Application entity. As each step is completed, Progress is used to capture each individual step along with those that have already occurred. To achieve this tracking, the current state of Progress is not altered. Rather, the current state of the previous steps, if any, is combined with the new step to form a new Progress state. This maintains the value immutability constraint.

The logic of a value being immutable can be reasoned on when considering the value 1. The value 1 cannot be altered; it is always the value 1. It would make no sense if the value 1 could be altered to be the value 3 or 10 instead. This is not a discussion about an integer variable, such as total, that can hold the value 1. Of course, a mutable variable total itself can be changed—for example, by assigning it to hold the value 1 and later to hold the value 3 and then, later still, the value 10. The difference is that while the variable total can be changed, the immutable value that it holds, such as 1, is constant. Although a Progress state is more complex than an integer, it is designed with the same immutability constraint. A Progress state cannot be altered, but it can be used to derive a new value of type Progress.

Aggregates

Generally, some business rules will require certain data within a single parent object to remain consistent throughout the parent object’s lifetime. Given that there is the parent object A, this is accomplished by placing behavior on A that simultaneously changes the data items that are managed within its consistency boundary. This kind of behavioral operation is known as atomicity; that is, the operation changes an entire subset of A’s consistency-constrained data atomically. To maintain the consistency in a database, we can use an atomic translation. An atomic database translation is meant to create isolation around the data being persisted, similar to A’s atomic behavioral operation, such that the data will always be written to disk without any interruption or change outside of the isolation area.

In a domain modeling context, an Aggregate is used to maintain a parent object’s transactional boundary around its data. An Aggregate is modeled with at least one Entity, and that Entity holds zero or more other entities and at least one Value Object. It composes a whole domain concept. The outer Entity, referred to as the root, must have a global unique identity. That’s why an Aggregate must hold at least one value, its unique identity. All other rules of an Entity hold for an Aggregate.

In Figure 7.2, PolicyQuote is the root entity that manages the concept’s transactional boundary. The transactional boundary is needed because when an Aggregate is persisted to the database, all business state consistency rules must be met when the atomic database transaction commits. Whether we’re using a relational database or a key–value store, the full state of a single Aggregate is atomically persisted. The same might not be possible for two or more such entity states, which emphasizes the need to think in terms of Aggregate transactional boundaries.

Image

Figure 7.2 A Domain Service is responsible for guiding a small business process.

Domain Services

Sometimes a modeling situation calls for an operation to apply business rules that are not practical to implement as a member of the type on which it operates. This will be the case when the operation must supply more behavior than a typical instance method should, when the operation cuts across two or more instances or the same type, and when the operation must use instances of two or more different types. In such a modeling situation, use a Domain Service. A Domain Service provides one or more operations that don’t obviously belong on an Entity or a Value Object as would normally be expected, and the service itself owns no operational state as does an Entity or Value Object.

Figure 7.2 shows the Domain Service named QuoteGenerator. It is responsible for accepting one parameter value named PremiumRates, translating it to one or more QuoteLine instances (which comprise a specific coverage and premium), and recording each on the PolicyQuote Aggregate. Note that when the final QuoteLine is recorded, the QuoteGenerated event is emitted.

An important difference between an Application Service and a Domain Service is that an Application Service must not contain business logic, while a Domain Service always does. If an Application Service is used to coordinate this use case, it would manage the transaction. Either way, the Domain Service would never do that.

Functional Behavior

Many approaches to domain modeling exist, but to date the object-oriented paradigm has completely dominated. There are ways,1 however, of expressing domain behavior using pure functions rather than mutable objects. The section “Functional Core with Imperative Shell,” in Chapter 8, “Foundation Architecture,” discusses the benefits of leveraging the Functional Core approach, which emphasizes using pure functions for domain models. The result is that the code is more predictable, more easily tested, and easier to reason about. While it is easier to express functional behaviors in purely functional language, the good news is that there is no need to switch away from an object-oriented language to a functional one, because functional fundamentals can be used with practically any programming language. Contemporary object-oriented languages, such as Java and C#, incorporate functional constructs that enable functional programming even beyond the fundamentals. Although they don’t have all the features of a full functional language,2 implementing a domain model with functional behavior is readily achievable with such languages.

2 Functional programming features have been added to several object-oriented languages as new versions have streamed out to the market. This has resulted in multiparadigm languages, which make it easier to switch from object-oriented programming to functional programming, and back. Questions remain about how effective mixing paradigms in a language is, and whether it way hinders the expression of the business software model under development.

1 Some people would say “better ways” to highlight the fact that the recent hype around the functional programming paradigm might make object-oriented programming appear to be outdated. However, the authors stress that each programming paradigm has its strengths and weaknesses, and that no single paradigm excels in every use case.

Domain modeling using the Domain-Driven Design approach works well with models expressed as functional behavior. Eric Evans, in his book Domain-Driven Design [DDD-Evans], stresses the importance of using “side-effect-free functions” to avoid unwanted and unintended consequences of calling an arbitrarily deep nesting of side-effect-generating operations. In fact, Value Objects, as described above, are an example of one way to implement functional behavior. When side effects are not avoided, the results cannot be predicted without a deep understanding of the implementation of each operation. When the complexity of side effects is unbounded, the solution becomes brittle because its quality cannot be ensured. The only way to avoid such pitfalls is by simplifying the code, which might include employing at least some functional behavior. Functional behavior tends to be useful with less effort. (There are plenty of examples of poorly written code in both paradigms, so don’t expect any silver bullets delivered with a functional programming language.)

Consider an example of quoting a policy based on the risk and rate. The NuCoverage team working on the Underwriting Context has decided to use a functional behavior approach when modeling PolicyQuote:

public record PolicyQuote
{
    int QuoteId;
    int ApplicantId;
    List<QuoteLine> QuoteLines;
}

In this example, the PolicyQuote is modeled as an immutable record. Immutability describes the means of creating the record such that its state cannot be changed. This has at least two benefits:

PolicyQuote instances can be passed to different functions without any fear that a function will modify their state. The unpredictability associated with mutable objects is eliminated.

▪ Instances can be shared in concurrent environments across threads because their immutable nature negates locking and the potential for deadlocks.

Even though these points might not be the main decision drivers for the NuCoverage team, it is helpful to understand that these benefits are available for free when immutability is designed in.

Defining immutable structures for domain concepts is the first step when employing functional behavior. Following this, NuCoverage must define the concrete behaviors with pure functions. One of those operations is recording QuoteLine instances to be composed by PolicyQuote. With the traditional OOP approach, a RecordQuoteLine operation would be invoked on a PolicyQuote instance, with its internal state being mutated to record the new QuoteLine. With the functional behavior approach, this kind of behavior is not permitted because PolicyQuote is immutable and once instantiated cannot be changed. But there is another approach, which does not require any mutation at all. The RecordQuoteLine operation can be implemented as a pure function:

public Tuple<PolicyQuote, List<DomainEvent>> RecordQuoteLine(
  PolicyQuote policyQuote,
  QuoteLine quoteLine)
{
  var newPolicyQuote =
        PolicyQuote.WithNewQuoteLine(policyQuote, quoteLine);

  var quoteLineRecorded =
        QuoteLineRecorded.Using(quoteId, applicantId, quoteLine);

  return Tuple.From(newPolicyQuote, quoteLineRecorded);
}

In this example, RecordQuoteLine is defined as a pure function: Every time the function is called with the same values, it yields the same result. This is meant to work similarly to the pure mathematical function, where the function power = x * x will always yield 4, if 2 is passed as an input parameter. The RecordQuoteLine function simply returns a tuple3 composed of the new instance of PolicyQuote with a new QuoteLine appended to the new instance of the current collection of quote lines, along with a QuoteLineRecorded domain event.

3 A tuple represents a composition of two or more arbitrary types as a whole. Here, PolicyQuote and List<DomainEvent> are addressed as a single whole. Tuples are frequently used in functional programming because a function may return only one type of result. A tuple is a single value that holds multiple values.

Note that the input parameter policyQuote is never mutated. The primary advantage of this approach is that the RecordQuoteLine function has improved the predictability and testability of its behavior. There is no need to look into the implementation of this function, as there cannot be any observable side effect or unintended mutation of the current application state.

Another important benefit of the functional paradigm is that pure functions can be readily composed if the output type of the function to be composed matches the input type of the function composing it. The mechanics are beyond the scope of this book, but are provided in a follow-on implementation book, Implementing Strategic Monoliths and Microservices (Vernon & Jaskuła, Addison-Wesley, forthcoming).

The main takeaway of functional behavior is that it simplifies the surface of the abstractions used in the model, improving their predictability and testability. Given the capabilities of modern object-oriented languages and the benefits they offer, there is a strong incentive to use them. At least initially, attempt functional programming using the “Functional Core with Imperative Shell” approach.

Applying the Tools

Domain modeling is one of the best places to experiment, and is exemplified by the rapid event-based learning described in Chapter 3, “Events-First Experimentation and Discovery.” Experiments should be quickly performed, using names and behavioral expressions that best describe the business concepts being modeled. This can be done with sticky notes, with virtual whiteboards and other collaborative drawing tools, and—of course—in source code. These tools are applied in the remaining chapters of this book.

Many examples of these modeling techniques can be found in the books Implementing Domain-Driven Design [IDDD] and the quick-start guide Domain-Driven Design Distilled [DDD-Distilled].

Summary

This chapter introduced Domain-Driven Design tactical modeling tools, such as Entities, Value Objects, and Aggregates. The appropriate use of these tools depends on the modeling situations that they are intended to handle. Another modeling tool, functional behavior, is used to express domain model behavior using pure functions rather than mutable objects.

The primary messages within the chapter are as follows:

▪ Use an Entity when modeling a conceptual whole that is a uniquely identified individual thing and has a stateful life cycle.

▪ A Value Object encapsulates data attributes/properties that together compose a whole value, and provides side-effect-free behavior. Value Objects are immutable and make no attempt to identify themselves uniquely, but only by the whole value of their type and all combined attributes/properties.

▪ Aggregates model a whole concept that can be composed of one or more Entities and/or Value Objects, where a parent Entity represents a transactional consistency boundary.

▪ Use a Domain Service to model a business operation that is itself stateless and that would be misplaced if included as behavior on an Entity or Value Object.

▪ Functional behavior houses business rules in pure function, but doesn’t require the use of a pure functional programming language. In other words, a contemporary object-oriented language, such as Java or C#, can be used to implement pure functions.

This concludes Part II. We now transition to Part III, “Events-First Architecture.” which discusses software architectural styles and patterns that offer pragmatic purpose rather than technical intrigue.

References

[DDD-Distilled] Vaughn Vernon. Domain-Driven Design Distilled. Boston: Addison-Wesley, 2016.

[DDD-Evans] Eric Evans. Domain-Driven Design: Tackling Complexity in the Heart of Software. Boston: Addison-Wesley, 2004.

[IDDD] Vaughn Vernon. Implementing Domain-Driven Design. Boston: Addison-Wesley, 2013.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
52.15.59.163