Communication glitches happen all the time and software development is no different from any other area that involves two or more people (or computers) talking to each other. We’re using programming languages after all, aren’t we?
Many such situations come from the fact that we’re often being implicit about the way we describe solutions in code. Assumptions stay in our heads instead of being expressed directly. We’re too quick to dive into development without getting a deep understanding of the problem first. This, in severe cases, may even cause thousands of other developers to make dearly mistakes.
What is the Domain?
The common dictionary definition of domain is "a sphere of knowledge or activity", for instance the business the company runs. Drilling down a bit from that, domain in the realm of software engineering commonly refers to the subject area on which the application is intended to apply. A Domain is also called a "problem space", so the problem for which we have to design a solution. In other words, during application development, the domain is the "sphere of knowledge and activity around which the application logic revolves".
Another common term used during software development is the domain layer or domain logic, which may be better known to many developers as the business logic. The business logic of an application refers to the higher-level rules for how business objects interact with one another to create and modify modelled data.
Principles behind the DDD
In fact, to design a good software, it's important to know what that software is about.
One principle behind DDD is to bridge the gap between domain experts and developers by using the same language to create the same understanding. Another principle is to reduce complexity by applying object oriented design and design patters to avoid reinventing the wheel.
In software development, the domain driven design approach is used for complex needs, connecting the implementation to an evolving model of the core business concepts. DDD has a strategic value and it’s about mapping business domain concepts into software artifacts. It's about organizing code artifacts in alignment with business problems, using the same common, ubiquitous language.
Mainly, domain driven design focuses on:
- The core domain and domain logic.
- Complex designs on models of the domain.
- Improving the application model and resolving emerging domain-related issues by collaborating with domain experts.
Application architecture: Layers of DDD
DDD isn't a methodology, it's more about the software's architectural design, providing a structure of practices to take design decisions that help in software projects that have complicated domains.
There are 3 main layers you should know while developing your application:
- Domain Layer, that contains the infrastructure independent domain logic.
- Infrastructure Layer provides the technology dependent artifacts, like implementation to handle database requests and responses, or remote third-party services calls.
- Application Layer acts as a gateway to business logic with integrated transaction control.
Using this style of architecture, the Domain Layer of our business logic does not depend on anything. We can change implementation of Infrastracture Layer without affecting any business logic.
Domain
The Domain Layer contains the real business logic, but does not contain any infrastructure specific code. The infrastructure specific implementation is provided by the Infrastructure Layer. The Domain Model should be designed as described by the CQS(Command-Query-Separation) principle. There can be query methods which do just return data without affecting state, and there are command methods, which affect state but do not return anything.
<?php
// ...
class MarkTodoAsDoneAction extends TodoAction
{
// ...
public function __construct(ArrayAccess $payload)
{
$this->payload = $payload;
}
public function apply(TodoData $data): TodoData
{
Assertion::true($this->payload->has('todo_id'));
$todoId = $this->payload->uuid('todo_id');
/** @var TodoItem $todoItem */
$todoItem = $data->todoItem($todoId)->done();
$data = $data->withTodoItem($todoItem);
$event = new TodoWasMarkedAsDone($todoItem);
DomainEventPublisher::instance()->publish($event);
return $data;
}
}
Infrastructure
The Infrastructure Layer provides the infrastructure dependent parts for all other layers. Aggregate data can be stored in an RDMBS like Oracle or MySQL, or it can be stored as XML/JSON or even Google ProtocolBuffers serialized objects in a key-value or document based NoSQL engine. This is up to you, as long the storage provides transaction control and guarantees consistency. Infrastructure can be best described as "Everything around the domain model", so databases, file system resources or even Web Service consumers if we interact with other systems.
<?php
// ...
use AppTodoBundleDomainTodoRepository;
class DoctrineTodoRepository implements TodoRepository
{
// ...
public function ofId(TodoId $todoId): Todo
{
$row = $this->connection
->createQueryBuilder()
->select('*')
->from(static::TODO_TABLE)
->where('todo_id = :todo_id')
->setParameter('todo_id', (string) $todoId);
if ($row === false) {
throw TodoNotFound::byId($todoId);
}
return $this->todoSerializer->signupFromRow($row);
}
// ...
}
Application
The Application Layer takes commands from the Client Interface Layer and translates these commands to use case invocations on the Domain layer. The Application Layer also provides transaction control for business operations. The Application layer is responsible to translate Aggregate data into the client specific presentation model by a Mediator or Data Transformer pattern.
<?php
// ...
class UpdateTodoStatusService implements ApplicationService
{
// ...
public function execute(UpdateTodoStatusRequest $request): ApplicationResult
{
$todo = $this->todoRepository->ofId($request->todoId());
/** @var TodoAction|MarkTodoAsDoneAction */
$action = $todo->actionFactory()->todoActionOfId(
new ActionId('mark_todo_as_done'),
ArrayAccess::for($request->data())
);
$todo->act($action);
$this->todoRepository->save($todo);
return $todo->render($this->viewFactory);
}
}
Client / User Interface Layer
The Client Layer consumes Application Services and invokes business logic on these services. Every invocation is a new transaction.
The Client Layer can be almost anything, as the view controller to a SOAP web service endpoint or a RESTful web resource. Any framework or custom implementation can be used to create the user interface.
<?php
// ...
class TodoController extends BaseController
{
// ...
/**
* @param ApplicationService|UpdateTodoStatusService $updateTodoStatusService
*/
public function __construct(ApplicationService $updateTodoStatusService)
{
$this->updateTodoStatusService = $updateTodoStatusService;
}
/**
* @param UpdateTodoStatusRequest $request
*
* @throws TodoNotFound
* @throws TodoItemNotFound
* @throws BadRequest
*
* @return JsonResponse
*/
public function markTodoDone(UpdateTodoStatusRequest $request): JsonResponse
{
/** @var ApplicationResult $result */
$result = $this->updateTodoStatusService->execute($request);
return JsonResponse($result->data());
}
}
Common terms used for DDD
A Domain is a sphere of knowledge and is usually known as the problem space.
An Ubiquitous Language is used by all the team in order to connect all the activities of the team with the software. The ubiquitous language of DDD helps when it comes to knowing more about terms that are used by the business experts. The tech team is able to know if the language changes and if a specific term will be used for a different meaning.
A Bounded Context should be aligned with a Domain. There is one Ubiquitous Language applied within a Bounded Context. A Bounded Context is usually the solution space, where we design our software or business solution. And it is the best to align one problem space (Domain) with one solution space (Bounded Context).
A Context Map displays the alignment of Domains and their Bounded Contexts. A Context Map also shows dependencies between Bounded Contexts. Such dependencies can be upstream or downstream. Dependencies show where integration patterns should or must be applied. Context Map is the starting point for any further modeling.
Building blocks
Domain-driven design also defines a number of high-level concepts that can be used in conjunction with one another to create and modify domain models:
- An Entity is an object that is not defined by its attributes, but rather by a consistent thread of continuity and its identity.
Example: Most airlines distinguish each seat uniquely on every flight. Each seat is an entity in this context. However, some low-cost airlines do not distinguish between every seat; all seats are the same. In this context, a seat is actually a value object.
- A Value Object is an immutable (unchangeable) object that contains attributes but has no conceptual distinct identity.
Example: When people exchange dollar bills, they generally do not distinguish between each unique bill; they only are concerned about the face value of the dollar bill. In this context, dollar bills are value objects. However, the Federal Reserve may be concerned about each unique bill; in this context each bill would be an entity.
- An Aggregate is a collection of objects that are bound together by a root entity, otherwise known as an aggregate root. Rather than allowing every single entity or value object to perform all actions on its own, the collective aggregate of items is assigned a singular aggregate root item. External objects no longer have direct access to every individual entity or value object within the aggregate, but instead only have access to the single aggregate root item, and use that to pass along instructions to the group as a whole. Aggregates can also be seen as a kind of bounded context, giving the root entity and the whole object graph a context in which they are used.
Example: When you drive a car, you do not have to worry about moving the wheels forward, making the engine combust with spark and fuel, etc.; you are simply driving the car. In this context, the car is an aggregate of several other objects and serves as the aggregate root to all of the other systems. A steering wheel can be rotated, this is it's context within the car aggregate. It can also be produced or recycled. This usually happens not within the driving car context, so this would be another aggregate, probably referencing the car as well.
Repository is a service that uses a global interface to provide access to all entities and value objects that are within a particular aggregate collection. Methods should be defined to allow for creation, modification, and deletion of objects within the aggregate. However, by using this repository service to make data queries, the goal is to remove such data query capabilities from within the business logic of object models. Repositories are part of the domain model, so they should be database vendor independent. Repositories can use DAOs (Data Access Objects) for retrieving data and to encapsulate database specific logic from the domain. Repositories can use an Aggregate Oriented Database.
Domain Events is an object that is used to record a discrete event related to model activity within the system and they can be used to model distributed systems. While all events within the system could be tracked, a domain event is only created for event types which the domain experts care about. The model will become more complex, but it can be more scalable. Domain Events are often used in an Event Driven Architecture Service: When an operation does not conceptually belong to any object. Following the natural contours of the problem, you can implement these operations in services.
A Service is an operation or form of business logic that doesn't naturally fit within the realm of objects. In other words, if some functionality must exist, but it cannot be related to an entity or value object, it's probably a service. For instance, if the business logic invocation includes operation across multiple Domain Objects or in this case integration with another Bounded Context.
Example: The Bill Aggregate and the Bill Repository should be put into the same service, as they are very tightly coupled.
- Factory encapsulates the logic of creating complex objects and aggregates, ensuring that the client has no knowledge of the inner-workings of object manipulation. An alternative implementations may be easily interchanged.
Characteristics of a strong domain model
- Being aligned: with the business' model, strategies and processes.
- Being isolated: from other domains & layers in the business.
- Be loosely designed: with no dependencies on the layers of the application on either side of domain layer.
- Being reusable: to avoid models that are duplicated.
- Be an abstract and cleanly separated layer: to have a maintenance, testing, and versioning that is easier.
- Minimum dependencies on infrastructure frameworks: to avoid outliving those frameworks and getting tight coupling on external frameworks.
Advantages of DDD
Eases Communication: With an early emphasis on establishing a common and ubiquitous language related to the domain model of the project, teams will often find communication throughout the entire development life cycle to be much easier. Typically, DDD will require less technical jargon when discussing aspects of the application, since the ubiquitous language established early on will likely define simpler terms to refer to those more technical aspects.
Business necessities oriented: Everyone ends up using the same language & terms and the team is sharing a model. Developers communicate better with the business team and the work is more efficient when it comes to establishing solutions for the models that reflect how the business operates, instead of how the software operates.
A balanced application: With DDD you build around the concepts of the domain and around what the domain experts are advising. This implies that the applications developed will indeed represent what the domain needs instead of getting an application that is only focused on UX & UI, forgetting the rest of requirements. The most important is to get a balanced product that suits well the users/audience of that specific domain.
Keeping track made easy: it becomes quite simple to keep track of requirement implementation, if everyone is using the same terminology.
Improves Flexibility: Since DDD is so heavily based around the concepts of object-oriented analysis and design, nearly everything within the domain model will be based on an object and will, therefore, be quite modular and encapsulated. This allows for various components, or even the entire system as a whole, to be altered and improved on a regular, continuous basis.
A common set of terms and definitions used by the entire team: Teams find communication much easier during the development cycle because from the beginning, they focus on establishing the ubiquitous language that is common to both parties (development & business experts). The language is linked to the domain model of the project and technical aspect are referred to through simples terms that all understand.
Emphasizes Domain Over Interface: Since DDD is the practice of building around the concepts of domain and what the domain experts within the project advise, DDD will often produce applications that are accurately suited for and representative of the domain at hand, as opposed to those applications which emphasize the UI/UX first and foremost. While an obvious balance is required, the focus on domain means that a DDD approach can produce a product that resonates well with the audience associated with that domain.
Disadvantages of DDD
Requires Robust Domain Expertise: Even with the most technically proficient minds working on development, it’s all for naught if there isn’t at least one domain expert on the team that knows the exact ins and outs of the subject area on which the application is intended to apply. In some cases, domain-driven design may require the integration of one or more outside team members who can act as domain experts throughout the development life cycle.
Encourages Iterative Practices: While many would consider this an advantage, it cannot be denied that DDD practices strongly rely on constant iteration and continuous integration in order to build a malleable project that can adjust itself when necessary. Some organizations may have trouble with these practices, particularly if their past experience is largely tied to less-flexible development models, such as the waterfall model or the like.
Ill-Suited for Highly Technical Projects: While DDD is great for applications where there is a great deal of domain complexity (where business logic is rather complex and convoluted), DDD is not very well-suited for applications that have marginal domain complexity, but conversely have a great deal of technical complexity. Domain complexity should be limited to one goal; this requires the creation of a microservice architecture. Since DDD so heavily emphasizes the need for (and importance of) domain experts to generate the proper ubiquitous language and then domain model on which the project is based, a project that is incredibly technically complex may be challenging for domain experts to grasp, causing problems down the line, perhaps when technical requirements or limitations were not fully understood by all members of the team.
Naming and architectural convension
Do not split Exeptions, Interfaces, Trait, etc. to subfolders or subnamespaces
The code in the domain should be focused on business logic, and not on the features of a programming language, like Exeptions, Interfaces or Traits. Code separation should be based on building blocks, implementation logic or semantic meaning.
No extra suffixes needed
Tautologies are common in everyday language, and when unintentional, they are often considered a fault of style. Tautology is present in programming, too. There is absolutely no need to add needless suffixes to naming, as it is already defined while defining them:
interface ProfileRepositoryInterface
{}
trait AwareMemberDataTrait
{}
class CustomException extends Exception
{}
It also makes no sense when describing UML diagrams, since you already indicate this on it. Also modern IDEs are doing a similar thing by visualizing files to simplify your work.
Naming should be descriptive
Suffix in Exception names is still a form of tautology, because wording and the context in which it is used (throw
, catch
, $exexption
variable name) unambiguously indicate that we are dealing with an Exception:
try {
throw CannotReopenTodoException::notDone($todo);
} catch (CannotReopenTodoException $ex) {
}
Event is another concept that is an indispensable part of event-driven and event-sourced architectures, and the same principle applies for them and most of the other domain elements that resides in their own namespace. The importance of the wording is even more pronounced in their case. Accurate description for event told in the past tense such as TodoWasMarkedAsDone
is self-evident.
$event = new TodoWasMarkedAsDone($todo);
DomainEventPublisher::instance()->publish($event);
No prefix for getters
get
prefix becomes excessive and provides no value to the consumer other than grouping all the property accessors with a common prefix.
<?php
// ...
final class TodoItem
{
// ...
public function getId(): TodoId
{
}
public function getDescription(): string
{
}
public function getStatus(): TodoStatus
{
}
}
Alternative "strip naked" naming convention for property accessor methods is a step closer to what will soon be a standard way of writing entities and value objects. Since the Typed Properties RFC has been accepted and feature will be available in PHP 7.4, and hopefully Read-only Properties RFC will go equally well, in the near future we will be able write our classes as follows:
<?php
// ...
final class TodoItem
{
public TodoId readonly $id;
public string readonly $description;
public TodoStatus readonly $status;
public function __construct(TodoId $id, string $description, TodoStatus $status)
{
$this->id = $id;
$this->description = $description;
$this->status = $status;
}
// ...
}
No prefixes for setters
Instead of adding set
prefixes we should implement better logic to make our entities more descriptive. Methods should describe what we are doing for entity, but not for object in general.
<?php
// ...
final class TodoItem
{
// ...
public function hasTodoId(): bool
{
return $this->todoId !== null;
}
/**
* @throws AssertionFailed
*
* @return TodoId
*/
public function todoId(): TodoId
{
Assertion::notNull($this->todoId);
return $this->todoId;
}
public function withTodoId(TodoId $todoId): self
{
$instance = clone $this;
$instance->todoId = $todoId;
return $this;
}
public function withoutTodoId(): self
{
$instance = clone $this;
$instance->todoId = null;
return $this;
}
}
As you can see from code example above, we are keeping our object immutable and prevent default PHP behavior by passing object by reference.
TL;DR: Conclusion
Domain-driven Design has a really great idea behind it. Using this technique, even very complex domain logic can be easily distilled and modeled. This leads to better systems, improved user experience and also more reliable and maintainable solutions.
DDD encompasses a common language, techniques and patterns as well as an architecture. The focus is put on the business and on modelling the problems that are trying to be solved. With DDD, the developers get techniques that will help minimizing the complexity and that will help driving collaboration with the rest. The idea is to use the requirements and to map out the business processes into the model by using the same language that is used by the business itself.