DDD Notes for Beginners - Part 1: Ports, Adapters, and Event-Driven Architecture (EDA)
ports - adapters - Event-Driven Architecture (EDA) and the Pub/Sub
Note: This content is currently under review and subject to updates as I continue to refine my understanding and gather more insights. Please consider this when reading, and feel free to contribute any suggestions or corrections you may have. Your feedback is highly appreciated and will help improve the accuracy and depth of this discussion.
Introduction
Welcome to the first part of our Domain-Driven Design (DDD) series for beginners. In this post, we'll explore the concepts of Ports and Adapters, and delve into Event-Driven Architecture (EDA) with a focus on the Pub/Sub model.
Some terminologies (consider a Library Management System Example)
Domain: Managing a library's operations.
Sub-Domains: Catalog Management, Membership Management, Borrowing Management.
Entities: Book (title, author, ISBN, status), Member (ID, name, membership date).
Aggregates: Library (books and members).
Domain Service: BorrowingService (handles borrowing and returning books).
Value Objects: Address (street, city, zip code).
Adapters in Domain-Driven Design (DDD)
Definition: Adapters are concrete implementations of ports. They connect the application to external systems like databases, web services, or messaging systems.
Key Points
Ports and Adapters Pattern:
Ports: Define interfaces that decouple the core application logic from external systems.
Adapters: Implement these interfaces to connect to specific external systems.
Separation of Concerns:
Core Logic: Remains independent of external systems, focusing purely on business rules.
Adapters: Handle the details of interacting with external systems, making it easier to replace or modify these interactions without affecting core logic.
Example
To illustrate the concept, let's consider an application that needs to fetch user data from a remote API and save it to a database. Here's how we can define and implement ports and adapters for this scenario:
Define Ports (Interfaces)
UserRepository:
interface UserRepository {
saveUser(user: User): Promise<void>;
}
===== Also =====
UserApiClient:
interface UserApiClient {
fetchUser(userId: string): Promise<User>;
}
Define Adapters (Concrete Implementations)
Database Adapter:
class DatabaseUserRepository implements UserRepository {
async saveUser(user: User): Promise<void> {
// Logic to save user to the database
console.log(`Saving user ${user.id} to the database`);
}
}
API Adapter:
class RemoteUserApiClient implements UserApiClient {
async fetchUser(userId: string): Promise<User> {
// Logic to fetch user from the remote API
console.log(`Fetching user ${userId} from remote API`);
return { id: userId, name: 'John Doe' }; // Mocked user data
}
}
Application Service Using Ports
UserService:
class UserService {
private readonly userRepository: UserRepository;
private readonly userApiClient: UserApiClient;
constructor(userRepository: UserRepository, userApiClient: UserApiClient) {
this.userRepository = userRepository;
this.userApiClient = userApiClient;
}
async fetchAndSaveUser(userId: string): Promise<void> {
const user = await this.userApiClient.fetchUser(userId);
await this.userRepository.saveUser(user);
}
}
Key Takeaway:
Adapters provide a bridge between the core application logic and external systems, ensuring a clean separation of concerns. This makes the system more modular, maintainable, and flexible in accommodating changes or new integrations.
Event-Driven Architecture (Pub/Sub Model)
Definition: Event-Driven Architecture (EDA) is a design pattern where the flow of the program is determined by events. An event can be defined as a significant change in state. The Pub/Sub (Publish/Subscribe) model is a common implementation of EDA where:
Publishers: Generate and send events.
Subscribers: Listen for and react to events.
Key Concepts
Events: Notifications of changes or significant actions (e.g., "OrderPlaced", "UserRegistered").
Publishers: Components that emit events when certain actions occur.
Subscribers: Components that respond to specific events, performing tasks or triggering other processes.
Event Bus: A mechanism that routes events from publishers to subscribers.
Simple Illustration and Example
Scenario: A user places an order on an e-commerce site.
Order Service: Publishes an event when an order is placed.
Inventory Service: Subscribes to the order placed event to update inventory.
Notification Service: Subscribes to the order placed event to send a confirmation email.
Illustration:
Event-Driven Architecture (Pub/Sub Model) with Hard-Coded Example
Concepts:
Events: Notifications of changes or significant actions (
e.g., "BookBorrowed", "BookReturned", "BookAdded").
Publishers: Components that emit events when certain actions occur.
Subscribers: Components that respond to specific events, performing tasks or triggering other processes.
Event Bus: A mechanism that routes events from publishers to subscribers.
Components and Workflow
Event Definition: Define the structure of the events. Event Handlers: Define the actions to be performed when events occur. Event Publisher: Component responsible for publishing events. Main Application: Component that uses the event publisher to emit events.
Step-by-Step Example
1. Define Event Handlers : (Subscribers):
Define specific actions to take when events occur. These are defined in
eventHandlers.js.
For example, when a
BookBorrowedEvent
is published, handlers update the book's status, send notifications, and log the event.
eventHandlers.js:
const eventHandlers = {
'BookBorrowedEvent': [
(bookData) => {
// Update the book's status in the database
console.log(`Updating status to borrowed for book ${bookData.id}`);
// Imagine this function updates the status in the database
},
(bookData) => {
// Send a notification to the user who borrowed the book
console.log(`Sending notification to ${bookData.user} for book ${bookData.title}`);
// Imagine this function sends a notification
},
(bookData) => {
// Log the book borrowing event
console.log(`Logging borrow event for book ${bookData.id}`);
// Imagine this function logs the event
}
],
'BookReturnedEvent': [
(bookData) => {
// Update the book's status in the database
console.log(`Updating status to available for book ${bookData.id}`);
// Imagine this function updates the status in the database
},
(bookData) => {
// Send a notification to the librarian
console.log(`Sending return notification for book ${bookData.title}`);
// Imagine this function sends a notification
}
],
'BookAddedEvent': [
(bookData) => {
// Add the new book to the catalog
console.log(`Adding book ${bookData.title} to the catalog`);
// Imagine this function adds the book to the catalog
},
(bookData) => {
// Update the total book count
console.log(`Updating total book count for the library`);
// Imagine this function updates the total book count
},
(bookData) => {
// Log the book addition event
console.log(`Logging addition of book ${bookData.title}`);
// Imagine this function logs the event
}
]
};
export default eventHandlers;
2. Define Events:
Represent significant actions or changes in the system.
Examples include
BookBorrowedEvent
,BookReturnedEvent
, andBookAddedEvent
.
events.js:
class BookBorrowedEvent {
constructor(bookData) {
this.bookData = bookData;
}
}
class BookReturnedEvent {
constructor(bookData) {
this.bookData = bookData;
}
}
class BookAddedEvent {
constructor(bookData) {
this.bookData = bookData;
}
}
export { BookBorrowedEvent, BookReturnedEvent, BookAddedEvent };
3. Define Event Publisher:
The
EventPublisher
class routes events to their corresponding handlers.When an event is published, the
publish
method looks up the event type and calls each registered handler with the event data.
eventPublisher.js:
import eventHandlers from './eventHandlers.js';
class EventPublisher {
publish(event) {
const eventName = event.constructor.name;
if (eventHandlers[eventName]) {
for (const handler of eventHandlers[eventName]) {
handler(event.bookData);
}
}
}
}
export default EventPublisher;
4. Main Application
app.js:
Key Note: In the Event-Driven Architecture, events can be seen as ports, and event handlers and publishers can be seen as adapters. This relationship ensures that the core application remains decoupled from the external systems, promoting modularity and flexibility.
Thank youuuu shaza, awesome post ♥