Sample DDD explanation in Ruby with Event-sourcing and Event-driven development
Domain Driven Design (DDD) is an approach to software engineering that focuses on modelling the domain of a business problem to ensure effective design and implementation of information systems.
In DDD, business language and an understanding of the problem domain play a critical role. System developers should work with domain experts to develop a common language and domain model that addresses real business needs.
DDD is based on a set of design patterns that help in designing information systems, such as Aggregate, Entity, Value Object, Repository, and Factory. An important aspect of DDD is the division of the system into layers, such as presentation, application, domain, and infrastructure layers.
The benefits of Domain Driven Design include
- A better understanding of the business problem domain by system developers
- Flexibility and ease of making changes to the system
- Easy testing and verification of the system
- Improved scalability and modularity of the system
- Improved code readability and understandability.
One of the key principles of DDD is that business rules should be the primary focus of the information system, not technology or programming tools. This makes the system more efficient and flexible, and its development less costly and more effective in the long run. In this article, we want to focus more on how to get our sticky notes into the code than on how to plan and prepare for meetings with the business. Let's start with that example:
# Value object representing a product SKU
class ProductSku
attr_reader :value
def initialize(value)
raise ArgumentError, 'Invalid SKU' unless valid?(value)
@value = value
end
def ==(other)
other.is_a?(self.class) && value == other.value
end
private
def valid?(value)
# Logic to validate the SKU value
true
end
end
# Entity representing a product
class Product
attr_reader :sku, :name, :description
def initialize(sku, name, description)
@sku = sku
@name = name
@description = description
end
end
# Aggregate root representing a collection of products
class ProductCatalog
def initialize(products = [])
@products = products
end
def add_product(product)
@products << product
end
def find_by_sku(sku)
@products.find { |p| p.sku == sku }
end
end
# Factory for creating Product entities
class ProductFactory
def self.create(sku, name, description)
Product.new(ProductSku.new(sku), name, description)
end
end
# Repository for storing and retrieving Product entities
class ProductRepository
def initialize
@catalog = ProductCatalog.new
end
def save(product)
@catalog.add_product(product)
end
def find_by_sku(sku)
@catalog.find_by_sku(ProductSku.new(sku))
end
end
# Create a new product
product = ProductFactory.create(name: "Example Product", price: Money.new(1000, "USD"))
# Save the product to the repository
ProductRepository.save(product)
# Load the product from the repository
loaded_product = ProductRepository.find_by_id(product.id)
# Update the product's price
new_price = Money.new(1200, "USD")
loaded_product.change_price(new_price)
# Save the updated product to the repository
ProductRepository.save(loaded_product)
In this example, we have:
ProductSku
as a value object representing a product SKU.Product
as an entity representing a product with properties such as SKU, name, and description.ProductCatalog
as an aggregate root representing a collection of products. It contains methods for adding products and finding products by SKU.ProductFactory
as a factory for creating newProduct
entities.ProductRepository
as a repository for storing and retrievingProduct
entities using theProductCatalog
.
Note that in this example, the ProductCatalog
serves as the aggregate root for the collection of products, and all interactions with the Product
entities made through the ProductCatalog
.
What is the Event-driven design?
Domain-driven design (DDD) emphasizes the importance of modelling the business domain and understanding the business problem. By using event-driven design (EDD) in conjunction with DDD, changes in the business domain can be effectively propagated throughout the system, ensuring that all relevant components can respond and adapt accordingly. In essence, the event-driven design complements domain-driven design by providing a way to propagate changes in the domain and enable components to respond to those changes in a timely and efficient manner.
class EventPublisher
def self.publish(event)
event_type = event.class.name
payload = event.to_h
KafkaProducer.send(event_type, payload.to_json) # or similar tool to publish events
end
end
# Define the product created event
class ProductCreatedEvent
attr_reader :product
def initialize(product)
@product = product
end
end
# Define the product price changed event
class ProductPriceChangedEvent
attr_reader :product, :old_price, :new_price
def initialize(product, old_price, new_price)
@product = product
@old_price = old_price
@new_price = new_price
end
end
# Create a new product
product = ProductFactory.create(name: "Example Product", price: Money.new(1000, "USD"))
# Publish the product created event
product_created_event = ProductCreatedEvent.new(product)
EventPublisher.publish(product_created_event)
# Save the product to the repository
# this could be caught by an event handler than inline (for example kafka consumer)
ProductRepository.save(product)
# Load the product from the repository
loaded_product = ProductRepository.find_by_id(product.id)
# Update the product's price
old_price = loaded_product.price
new_price = Money.new(1200, "USD")
loaded_product.change_price(new_price)
# Publish the product price changed event
product_price_changed_event = ProductPriceChangedEvent.new(loaded_product, old_price, new_price)
EventPublisher.publish(product_price_changed_event)
# Save the updated product to the repository
# this could be caught by an event handler than inline (for example kafka consumer)
ProductRepository.save(loaded_product)
In this example, we've introduced two event classes: ProductCreatedEvent
and ProductPriceChangedEvent
. When a new product is created, we publish a ProductCreatedEvent
using an EventPublisher
(which could be implemented as a message queue like kafka/rabbitmq or using a simple DB table). Similarly, when a product's price is changed, we publish a ProductPriceChangedEvent
.
Using this approach gives us some additional benefits:
- History of changes (because we're storing historical events)
- We can eliminate the repository that stores the product in the database because we can rebuild the state of the product using historical events. In the event-sourcing world, it's called projections.
In this example, EventStream
is an iterator that returns all the events published by EventPublisher
. Using the ProductProjection
class, we will iterate through all the events, and finally it will give us the latest state of the product
object.
By communicating events between different parts of the system, we can decouple them and make them more scalable and flexible. Other parts of the system can subscribe to these events and respond to them, for example, by sending notifications or updating related data-all with a nice list of historical changes - and that is the power of event-driven development.