An asynchronous response to data changes can be achieved in systems that abstract data persistence by combining the Repository Pattern with Event-Driven Architecture. This combination is especially helpful for systems that require certain actions, including updating external systems, logging changes, or sending notifications, to be initiated following database operations.
Now let's combine the two patterns into an example.
Step-by-Step Implementation:
1️⃣ Define a Repository for User Entities
2️⃣ Emit Events When Data Changes
3️⃣ React to Events Asynchronously
1. User Repository with Event Emission
We’ll extend the previous UserRepository example to emit events when certain actions, like user creation, take place.
class EventDrivenUserRepository extends InMemoryUserRepository {
private eventEmitter: EventEmitter;
constructor(eventEmitter: EventEmitter) {
super();
this.eventEmitter = eventEmitter;
}
async save(user: User): Promise<void> {
await super.save(user);
this.eventEmitter.emit('userCreated', user); // Emit event on user creation
}
}
Here, we extend the InMemoryUserRepository
to emit a userCreated
event whenever a new user is saved.
2. Event Subscriber to Handle Business Logic
We can now define event subscribers that will listen for specific events and react asynchronously.
const eventEmitter = new EventEmitter();
const userRepository = new EventDrivenUserRepository(eventEmitter);
// React to the 'userCreated' event
eventEmitter.on('userCreated', (user: User) => {
console.log(`New user created: ${user.name}`);
// Perform additional tasks, like sending an email
});
3. Combining Repository and Event-Driven in Practice
Let’s put everything together and simulate saving a user and reacting to the event:
(async () => {
const user = { id: '2', name: 'Jane Smith', email: 'jane@example.com' };
await userRepository.save(user);
})();
In this scenario, saving a new user through the userRepository will trigger the userCreated event, and the subscriber will execute the associated logic (e.g., logging or sending notifications) asynchronously.
Advantages of Combining Repository and Event-Driven Patterns
👉🏻 Decoupled Architecture: The repository is responsible only for data access, while business logic (like sending notifications or processing external updates) is handled by the event listeners. This separation of concerns leads to more maintainable code.
👉🏻 Scalability: Asynchronous event handling allows the system to scale better. You can easily add more event listeners to react to different events without changing the core repository logic.
👉🏻 Flexibility: The system becomes more flexible, as you can add or remove event listeners without modifying the core business logic.
For example, you can add new actions triggered by user creation, such as notifying external services, without altering the user repository.
Conclusion
Repository Pattern and Event-Driven Architecture work together to create incredibly scalable and decoupled systems. Events enable asynchronous communication and system responses to state changes, while the repository offers an abstraction for data access. When combined, these patterns help you build code that is easier to maintain, more readable, and capable of handling reactive logic and real-time updates.
This combination is especially helpful for sophisticated, distributed systems where separation of responsibilities and asynchronous processing are crucial, regardless of whether you're developing microservices or a monolithic system.
Top comments (2)
Hi Wallace Freitas,
Thanks for sharing.
Welcome João.