Arrange Act Assert

Jag Reehals thinking on things, mostly product development

Whose Responsibility Is It Anyway? Defining Boundaries with Event-Driven Architecture

20 Jan 2025

Defining clear boundaries is essential to building clean, scalable, and reliable architectures.

In my experience, organisational demands often override the focus on boundaries and domain-driven design. This reflects the tension between following technical best practices and delivering business outcomes quickly. While theoretical approaches are widely discussed at conferences, in books, and in videos, the practical implementation of these ideas is often shaped by cultural dynamics, resource constraints, tight deadlines, and internal politics.

In this post, we'll explore the challenges of maintaining boundaries and potential solutions, using a payment system as an example. This system facilitates transactions between clients and payment providers, such as PayPal or Stripe, highlighting the distribution of responsibilities within its architecture.

Why Stateless Systems Are Key to Loosely Coupled Architecture

Stateless systems are a ket component of loosely coupled architecture. By avoiding unnecessary state storage, they offer several compelling benefits:

  1. Focus on Core Functions:

    • Stateless systems adhere to the single responsibility principle, keeping them focused on their primary task.
  2. Scale More Easily:

    • With minimal state to track, these systems can handle higher loads without significant overhead.
  3. Simplify Communication:

    • Stateless systems exchange lightweight messages, often limited to essential identifiers like IDs, reducing complexity and ensuring efficient communication.

Take, for example, a payment system that links a client's payee ID to a payment provider's ID. This design avoids storing additional details, such as contact information, delegating that responsibility to the client. The client's database becomes the single source of truth, ensuring a cleaner and more reliable architecture.

flowchart LR
    Client[Client System] -->|Payee ID| Payments[Payments System]
    Payments -->|Provider ID| Provider[Payment Provider]

However, in my experience problems emerge when teams request the systems to take on responsibilities beyond its intended scope.

For instance, they might ask it to send emails to payees whenever details are updated. Although seemingly convenient, this approach can lead to significant architectural pitfalls.

Why Adding Contact Details is a Bad Idea

Allowing the payment system to store and manage contact information may seem harmless at first, but in my experience the convenience opens a Pandora's box and leads to significant problems later down the line:

  1. Duplication of Data:

    • Contact details must be synchronised between the client's database and the payment system. This increases the risk of discrepancies and data conflicts.
  2. Increased Complexity:

    • The payment system would require additional mechanisms to manage updates, validate data, and resolve conflicts.
  3. Misaligned Responsibilities:

    • Payment systems should focus on processing transactions, not managing user contact details. Expanding their role leads to bloated, harder-to-maintain codebases.
  4. Messaging Failures:

    • Another challenge arises when considering failures. What happens if the payment system tries to send an email and it fails? Should it handle retries, or should this responsibility lie elsewhere? Adding retry logic and failure handling shifts the focus of the payment system away from its core role of facilitating transactions. This responsibility is better suited to the client, who already has the necessary data.

Here's what happens when responsibilities blur:

flowchart TD
    subgraph "Problem: Duplicated Responsibilities"
        A[Client Database] -- Updates --> B[Payments System]
        B -- Sends Emails --> C[Payee]
        C -- Updates --> A
        B -- Retry Loop --> C
    end

To address these challenges and maintain clear boundaries, event-driven architecture offers an effective solution.

Event-Driven Architecture: A Scalable Solution

Event-driven architecture offers a clean, scalable solution for maintaining clear system boundaries while enabling flexible workflows.

The Problem: Overlapping Responsibilities

When systems take on responsibilities outside their intended scope, complexity and duplication increase, leading to maintenance challenges and inefficiencies.

For instance, if the payment system manages both transactions and user contact information or handles email notifications, it blurs boundaries and introduces unnecessary coupling.

The Solution: Clear Boundaries with Events

Event-driven architecture resolves this by enabling systems to remain focused on their core responsibilities while collaborating seamlessly through events.

  1. Raise Events:

    • The payment system publishes events like payee.updated or payment.successful when specific actions occur, passing only essential identifiers.
  2. Clients Handle Events:

    • The client system listens for these events, processes them, and performs actions like updating records or sending emails.
  3. Handle Failures Gracefully:

    • By delegating retry logic and error handling to the client system, failures in operations like email delivery are isolated from the payment system.

How It Works in Practice

  1. Payee Updates Details:

    • A payee updates their information with a payment provider.
  2. Payment System Emits an Event:

    • The system raises a payee.updated event containing only the relevant IDs.
  3. Client Listens and Processes:

    • The client system receives the event, updates its database, and triggers any necessary workflows, such as emailing.
  4. Retry Logic:

    • If an email fails to send, the client system handles retries without impacting the performance of the payment system.
sequenceDiagram
participant P as Payment Provider
participant PS as Payments System
participant C as Client System
participant E as Email Service

    P->>PS: Payment status update
    PS->>C: Emit payee.updated event
    Note over PS: Only passes IDs
    C->>C: Process event
    C->>E: Handle email (with retries)
    Note over C: Manages all contact details<br/>and messaging logic

Benefits of Event-Driven Architecture

This approach provides long-term benefits for scalability, maintainability, and system integrity.

  1. Maintainability
    Systems with focused responsibilities are easier to manage, test, and extend.

  2. Scalability
    Decoupled systems can scale independently, reducing bottlenecks and enhancing performance.

  3. Data Integrity
    By avoiding data duplication, systems ensure consistency and reduce the risk of conflicts.

  4. Customisation
    Client systems can handle domain-specific logic like email templates, personalisation, and language localisation without impacting the payment system.

This architecture ensures robust error handling, clean system boundaries, and a flexible foundation for future enhancements.

Handling Email Templates, Languages, and Personalisation

Beyond architectural benefits, event-driven systems also enable domain specific customisation, particularly for tasks like email notifications and user personalisation.

A client system handling email notifications has the added benefit of full control over:

  1. Email Templates:

    • Templates can be customised for different use cases, such as notifications, reminders, or promotions. This flexibility ensures that emails align with the client's branding and messaging strategy.
  2. Language Localisation:

    • The client system can dynamically select the appropriate language for each payee, ensuring clear communication regardless of their preferred language.
  3. Personalisation:

    • Personalised content (e.g., using the payee's name, specific account details, or tailored offers) can be included without requiring the payment system to store sensitive data.

By delegating this responsibility to the client, the system provides a better user experience while preserving clear boundaries and minimising complexity.

Conclusion

Clear boundaries between systems are essential for building scalable and maintainable architectures. Systems with focused responsibilities are easier to manage, test, and extend, while decoupled designs scale more effectively as each system evolves independently. Minimising duplication ensures consistency, avoids data conflicts, and reinforces the integrity of the overall system.

System design often requires balancing user demands with architectural constraints.

Event-driven architecture offers a powerful way to maintain these clear boundaries while addressing complex requirements. By adopting this approach, each system can focus on its core responsibilities, promoting flexibility, scalability, and reliability.

The principle I follow is simple: simplicity scales.

By embracing event-driven designs, we can create architectures that perform efficiently today and adapt to meet the demands of tomorrow.

events system-architecture