Quick overview and use case with EventFlow - DDD #1
Introduction
In the ever-evolving world of software architecture, one concept that stands out for its ability to manage complexity and align development with business needs is Domain-Driven Design (DDD). A brainchild of Eric Evans, DDD has emerged as a fundamental approach for professionals seeking to build robust, scalable systems that are deeply rooted in business realities. In this article, the first of a series, we embark on a journey to explore DDD, with a special focus on its implementation in .Net Core applications using the EventFlow library.
DDD is more than just a set of patterns and practices; it's a mindset, a strategic approach that puts the focus squarely on the core business domain and its logic. It advocates for a model-driven design, where the complexity of business processes is encapsulated in the code structure itself. This leads to systems that are more manageable, adaptable, and aligned with business goals.
Enter EventFlow, an open-source library designed for .Net Core that breathes life into DDD principles. EventFlow offers a comprehensive toolkit for building applications following DDD, CQRS (Command Query Responsibility Segregation), and event sourcing patterns. It's an arsenal for developers who want to write less boilerplate code and focus more on business logic.
But why just talk about DDD and EventFlow in the abstract when we can dive into a practical, albeit unconventional, case study? Throughout this series, we will unravel the intricacies of DDD and EventFlow through a whimsical yet educational scenario featuring a herd of goats. These goats, equipped with wallets, engage in an economic system where they can paint blank paintings, and sell paintings. This scenario is not only a fun way to explore complex concepts but also a testament to how DDD and EventFlow can simplify even the most bizarre business logics.
In this initial article, we set the foundation by providing a conceptual overview of DDD and introducing the core functionalities of EventFlow. As we progress through the series, we will delve deeper, exploring each concept with detailed C# .Net Core 8 examples and code snippets that breathe life into our goat-based economy.
By the end of this series, you’ll not only have a thorough understanding of DDD and EventFlow but also the practical know-how to implement these concepts in your own .Net Core applications. So, let’s embark on this journey of discovery and innovation, where goats, wallets, and paintings serve as our guides in the fascinating world of DDD with EventFlow.
When and why you don't need DDD
While Domain-Driven Design (DDD) offers significant advantages in managing complex software projects, there are situations where its application may not be necessary or even advisable. Understanding when not to use DDD is crucial for making efficient and effective architectural decisions. Here's when and why you might choose not to use DDD:
-
Small Applications or Prototypes: For small-scale applications, prototypes, or proof-of-concept projects where the primary goal is to validate an idea quickly, the extensive upfront investment in modeling and design required by DDD may slow down the development process unnecessarily.
-
Lack of Domain Complexity: DDD is particularly valuable in scenarios with high domain complexity. If the domain is not complex, the benefits of DDD diminish, and simpler development approaches could suffice and be more cost-effective.
-
Limited Resources or Expertise: DDD requires a certain level of expertise and experience, both in terms of software design and understanding the business domain. If a team lacks this expertise or is constrained in resources, implementing DDD might lead to suboptimal results.
-
Rapid Market Changes: In industries where market conditions and business requirements change rapidly, the time and effort invested in deep domain modeling may not pay off if the model becomes obsolete quickly.
-
Overhead for Small Teams: Small development teams might find the overhead of DDD's practices and abstractions burdensome, especially if they can achieve their goals with simpler methodologies.
-
Short-Lived Applications: For applications that are intended to be temporary or have a short lifespan, the investment in a DDD approach may not provide sufficient return on investment.
-
Limited Collaboration with Domain Experts: Successful DDD implementation requires close collaboration with domain experts. If such collaboration is not feasible or if domain expertise is not readily available, the benefits of DDD can be severely limited.
-
When YAGNI (You Aren’t Gonna Need It) Applies: Sometimes, over-engineering a solution can be a pitfall. DDD might lead to complex designs where a simpler solution would suffice. This is especially true in cases where future requirements are uncertain or speculative.
-
Projects with Well-Defined Frameworks or Libraries: If a project primarily involves integrating existing frameworks or libraries with minimal domain complexity, the DDD approach may not add significant value.
In summary, while DDD is a powerful tool for managing complex domains, it's not a one-size-fits-all solution. It's essential to assess the complexity of the domain, project size, team expertise, and the nature of the business requirements before deciding to implement DDD. For simpler projects, less complex architectures may be more appropriate and efficient.
So why using DDD ?
Domain-Driven Design (DDD) is a software development approach particularly valuable for tackling complex domains. Here are some key reasons why using DDD can be advantageous:
-
Alignment with Business Needs: DDD emphasizes close collaboration with domain experts to ensure the software accurately reflects real-world scenarios and business logic. This alignment helps create more relevant and effective software solutions that directly address business needs.
-
Complexity Management: DDD is adept at managing complex domains by structuring and organizing the domain model effectively. It helps break down a complex domain into more manageable parts (like Aggregates, Entities, Value Objects), making it easier to work with and understand.
-
Improved Communication: By using a ubiquitous language that is shared between developers and domain experts, DDD promotes clearer communication. This language is based on the domain itself, reducing misunderstandings and ambiguities in discussions and in the codebase.
-
Focus on Core Business Concepts: DDD encourages focusing on the core domain and its logic, rather than getting distracted by peripheral or technical concerns. This focus ensures that the most valuable and critical aspects of the software are given priority.
-
Enhanced Flexibility and Scalability: The modular nature of DDD makes it easier to scale and evolve the system over time. Changes in one part of the domain model can often be made with minimal impact on other parts, which is crucial for long-term maintenance and evolution.
-
Facilitates Iterative Development: DDD is well-suited to agile and iterative development processes. It allows for evolving models and continuous refinement based on real-world feedback, which is vital for adapting to changing business requirements.
-
Better Domain Insights: DDD helps uncover deeper insights into the domain, which can lead to more innovative solutions. Through the process of exploring and modeling the domain, hidden concepts and relationships often emerge, providing valuable new perspectives.
-
Reduced Risk of Failure: By ensuring that the software closely matches the domain complexities and business requirements, DDD reduces the risk of building the wrong thing. This alignment with business needs is crucial for delivering successful software projects.
-
Long-term Maintainability: A well-structured DDD application tends to be more maintainable in the long run. The separation of concerns, clear boundaries, and explicit models all contribute to a codebase that is easier to understand, modify, and extend.
-
Suitability for Complex Systems: DDD excels in situations where the domain is complex and the cost of failure is high. In such scenarios, the benefits of a deep understanding of the domain and a model that faithfully represents it are particularly significant.
In summary, DDD is chosen for its ability to effectively tackle complex domains, improve communication, align closely with business needs, and create software that is scalable, maintainable, and adaptable to changes.
Core Principles of DDD
At its heart, DDD focuses on the core domain and domain logic. It emphasizes a close alignment between the software code and the business realities it represents. This alignment is not just a technical endeavor but a collaborative one, involving both software developers and domain experts. The primary goal is to create a software model that is a faithful and useful representation of the domain, enabling developers to address complex scenarios with clarity and precision.
-
Aggregates: An aggregate is a cluster of domain objects that can be treated as a single unit. An example might be an order and its line items, where the order is the root of the aggregate and the line items are its elements. Aggregates define a boundary in terms of data and consistency; transactions should not cross these boundaries.
-
Entities: Entities are objects that are defined not by their attributes, but by a thread of continuity and identity. For example, a person remains the same entity despite changes to their attributes (like address or phone number). In DDD, entities have a distinct identity that runs through their lifecycle.
-
Value Objects: Value objects are objects that describe some characteristic or attribute but do not have a conceptual identity. They should be treated as immutable. For example, a color or a date range can be value objects.
-
Repositories: Repositories are mechanisms for encapsulating storage, retrieval, and search behavior, which emulates a collection of objects. They provide a way to access aggregate objects in a way that abstracts away the details of the data store.
-
Domain Events: Domain events are discrete events that signify something important has happened in the domain. For example, an event could be triggered when a new order is placed. These events are a key part of DDD as they help decouple different parts of the domain.
-
Services: In DDD, services are operations that don't naturally fit within the realm of an object. These typically involve some domain concept or domain logic that is not a natural part of an entity or value object. Services can be domain services (part of the domain model) or application services (which sit at a layer above the domain model).
DDD is a powerful approach, especially suited for complex domains where the cost of failure is high. It's not just a technical approach; it's a way of thinking and a methodology for aligning software design closely with the domain it's intended to serve. As we delve deeper into this series, we will explore these concepts in more detail, specifically in the context of EventFlow and its implementation in .Net Core. By understanding these foundational elements, we’re setting the stage for a comprehensive exploration of Domain-Driven Design and its practical applications.
DDD in a world of artist goats
In this chapter, we will explore the concept of Domain-Driven Design (DDD) aggregates through a progressive approach, starting with naive solutions and refining them. We describe several iterations to highlight fundamental DDD concepts like entities and aggregates.
Context
Our domain consists of goats, paintings, and wallets. The business rules are:
- A goat buys blank canvas.
- A goat can paint a blank canvas.
- A goat sells its painted canvas.
- A goat has a wallet representing a list of transactions from buying and selling paintings.
- A goat bleats when it has painted a canvas, signaling potential buyers.
We have a remote control with commands for buying a blank canvas, painting the canvas, and selling the canvas.
Entities Defined with Attributes
Let's start by defining the entities and their attributes.
GoatEntity {
_name
_paintings
_wallet
}
PaintingEntity {
_name
_state: [Blank, Painted, Sold]
_buyPrice
_sellPrice
}
WalletEntity {
_movements
}
The successive states of the paint can be represented on a state-transition diagram.
Initial Implementation: An Aggregate per Entity
First step, we design an aggregate per entity. Buying a blank painting consists in adding the painting into a collection. So the PaintingAggregate has a collection of paintings. Typically the persistence of changes occurs when the save method is called.
GoatAggregate {
_goat
Save()
}
PaintingAggregate {
_paintings
Save()
}
WalletAggregate {
_wallet
Save()
}
- PaintingAggregate for Buying a Canvas
Then we can add an handler for the BuyBlankPainting command of our remote control. Let's add it to the PaintingAggregate. The save method is called when we need to persist aggregate internal state changes, regardless of the command processing.
PaintingAggregate {
_paintings
HandleBuyBlankPainting(command){
_paintings.Add(new Painting())
}
Save()
}
- Handling Money Movement in WalletAggregate
The transaction must be added to the wallet.
At this step, we have several ways to handle this:
- the wallet may handle the buy blank painting command.
- the painting may address the wallet. To avoid coupling, we'd make it through an event.
Let's dive into the first solution and add an handler to the WalletAggregate:
WalletAggregate {
_wallet
HandleBuyBlankPainting(command){
_wallet.movements.Add(command.BuyPrice)
}
Save()
}
Here we reach the first problem in this design: we save in both aggregates so two transactions occur. This may lead to inconsistency: if an error happens when saving the wallet, the painting has no counterpart in the wallet. Also the need of the price led to add it to the command.
Diving into the second solution is useless since we'd keep both transactions. A way to keep consistency with this design is using sagas, at the cost of added complexity. The second solution has another drawback: if we need a result from the command addressed to the wallet, we can't have it.
Initial Misconception: An Aggregate per Entity
Three aggregates, one per entity.
Criticism: One transaction per aggregate, added complexity with sagas, added drawback with cascading events, command must possess all properties needed by all handlers.
Refined Solution: Single Aggregate Approach
To keep a single transaction, we need to manage the paintings and the wallet in the same aggregate. Based on this principle and on business rules, we should group the entities into the GoatAggregate. Let's add the paint and sell command handlers and compile. Let's take this opportunity to add some refinements with some error handling for instance.
GoatAggregate {
_wallet
_paintings
_goat
HandleBuyBlankPainting(command){
painting = new PaintingEntity()
_paintings.Add(painting)
_wallet.MoveMoney(-painting.BuyPrice))
}
HandlePaintPainting(command){
painting = _paintings.Select(command.PaintingName)
var result = painting.Paint()
if(result != Error) {
_goat.Bleat(command.PaintingName))
}
}
HandleSellPainting(command){
_paintings.Delete(command.PaintingName)
_wallet.MoveMoney(painting.SellPrice))
}
Save() {
// persist aggregate internal state changes
}
}
Definitions for GoatEntity, PaintingEntity and WalletEntity
GoatEntity {
_name
Bleat(paintingName) {
// bleat
}
}
PaintingEntity {
_name
_state: [Blank, Painted, Sold]
BuyPrice
SellPrice
Paint(){
switch(_state)
case Blank:
_state = Painted
case Painted:
return Error // choice opened between return value or exception. Monads possible.
}
}
WalletEntity {
_movements
MoveMoney(amount){
if(!IsBalancePositive(amount)){
throw exception
}
_movement.Add(amount)
}
IsBalancePositive(amount){
return GetBalance + amount > 0
}
GetBalance(){
return _movements.Sum()
}
}
Have a goat day! 🐐