How modular can your monolith go? Part 5 - decoupling domains with the Observer pattern

architecting   modular monolith  

This article is the fifth in a series of articles about modular monoliths. The other articles are:

In previous articles, I described how a domain’s API should consist of one or more narrowly-scoped interfaces, a.k.a. facades, that encapsulate its implementation. A domain’s client can invoke the domain by calling a facade’s methods. For example, the Order Management domain can reserve credit by invoking CreditManagement.reserveCredit(), which is implemented by the Customer Management domain. Similarly, the Customer Management domain can send a welcome email to a newly registered customer by invoking NotificationService.sendEmail(), which is implemented by the Notification Management domain.

But let’s imagine that sending an email is not the only post-registration action that Customer Management needs to implement. It might, for example, need to register the customer with the CRM system. Customer Management could simply invoke CRMIntegration.registerNewCustomer() directly. The drawback, however, is that each action adds another dependency of Customer Management, which is yet another reason for it to change.

In this article, I look at an alternative approach to API design that potentially minimizes the risk of tight-coupling: the Observer pattern.

The trouble with dependencies

Even though dependencies, or more specifically design-time dependencies, are unavoidable, it’s usually a good idea to minimize them. A software element (class, package, domain, …) often needs to collaborate with others in order to fullfil its responsibilities. The problem, however, is that each dependency is a potential reason to change. Even if each dependency has a stable API, the risk of a lock change is proportional to the number of dependencies. It’s therefore a worthwhile design goal to minimize the number of dependencies.

This is especially true if those dependencies are unrelated to the software element’s core responsibilities. For example, the Customer Management domain’s core responsibility is managing customers. Responsibilities such as sending emails and synchronizing with the CRM system are somewhat secondary.

Moreover, it’s desirable to be able to add a post-registration action without having to modify Customer Management. In other words, Customer Management should conform to the Open-Closed Principle and be open for extension but closed for modification.

Using the Observer pattern to decouple Customer Management from Notification Management

One way to decouple the Customer Management domain and to make it extensible is to use the Observer pattern. The Observer pattern, which is also known as Publish-Subscribe, is a classic object-oriented design pattern - see GOF page 293 - that allows an object (the subject) to notify other objects (the observers) when its state changes without having to know their details. Let’s look at how this pattern can be used to minimize Customer Management’s dependencies and enable it to conform to the Open-Closed Principle.

In this particular example, Customer Management domain is the subject and the post-registration actions are the observers. Customer Management only knows the abstract observer type, not the concrete observers. When a customer is registered, for example, Customer Management notifies the observers, which then perform their actions.

The following diagram shows the design:

The key parts of the design are:

  • CustomerDomainObservers - the interface that observers use to register themselves
  • CustomerDomainObserver - the interface that observers must implement
  • CustomerServiceImpl - the concrete subject class
  • NotificationServiceImpl - the concrete observer class

Let’s look at each one in more detail.

CustomerDomainObserver - the observer interface

CustomerDomainObserver is the interface that observers of the Customer Management domain must implement. It defines notification methods, such as noteCustomerCreated(), that are invoked by the Customer Management domain.

A key design decision is what information to pass to the observer. One option is for notification methods to have minimal parameters. The noteCustomerCreated() method could, for example, simply take a customerId argument. The observer could then retrieve additional information from the Customer Management domain.

The second option is for notification methods to provide a typical observer with what it needs. Each method could have additional arguments or, better yet, a single parameter object, since that avoids having to change the method signature when additional information is needed. For example:

public interface CustomerDomainObserver {
  void noteCustomerCreated(CustomerInfo customer);
}

Both options couple observers to the Customer Management domain. However, the first option might enable an observer to leverage the interface segregation principle (ISP) in order to reduce coupling. Different observers can use different APIs to retrieve the information they need from the Customer Management domain.

CustomerDomainObservers - the observer registration interface

CustomerDomainObservers is an interface that CustomerDomainObserver instances use to register themselves with the Customer Management domain. It defines a single method, register(), that takes a CustomerDomainObserver instance as an argument.

CustomerServiceImpl - the concrete subject class

CustomerServiceImpl is the concrete subject class. It implements CustomerDomainObservers. Its various methods notify the registered observers. The createCustomer() method, for example, invokes the noteCustomerCreated() method on each registered observer. It no longer calls, for example, NotificationService.sendEmail() directly.

NotificationServiceImpl - the concrete observer class

NotificationServiceImpl is a concrete observer class. It implements CustomerDomainObserver and registers itself with the Customer Management domain. The noteCustomerCreated() method, for example, sends a welcome email to the newly registered customer.

Relationship with events

The Observer pattern is equivalent to an event-based design. Each observer method corresponds to an event type. Perhaps the one difference is that event framework, e.g. Spring’s events, is more generic. You would, for example, only need a single implementation of the publish and subscribe API rather than per-subdomain subjects and subscribers.

Does inverting dependencies with the Observer pattern reduce coupling?

The main consequence of using the Observer pattern is that it inverts the dependencies.

Previously, Customer Management depended upon Notification Management (and potentially other domains). Now, Notification Management depends upon Customer Management. Customer Management has fewer dependencies, which is potentially beneficial. But this improvement has been achieved at the expense of Notification Management, which now has more dependencies. Which design results in fewer lockstep changes, is highly context specific and depends on the ways in which the domains are likely to change.

Need help with accelerating software delivery?

I’m available to help your organization improve agility and competitiveness through better software architecture: training workshops, architecture reviews, etc.

Learn more about how I can help


architecting   modular monolith  


Copyright © 2024 Chris Richardson • All rights reserved • Supported by Kong.

About Microservices.io

Microservices.io is brought to you by Chris Richardson. Experienced software architect, author of POJOs in Action, the creator of the original CloudFoundry.com, and the author of Microservices patterns.

ASK CHRIS

?

Got a question about microservices?

Fill in this form. If I can, I'll write a blog post that answers your question.

NEED HELP?

I help organizations improve agility and competitiveness through better software architecture.

Learn more about my consulting engagements, and training workshops.

LEARN about microservices

Chris offers numerous other resources for learning the microservice architecture.

Get the book: Microservices Patterns

Read Chris Richardson's book:

Example microservices applications

Want to see an example? Check out Chris Richardson's example applications. See code

Virtual bootcamp: Distributed data patterns in a microservice architecture

My virtual bootcamp, distributed data patterns in a microservice architecture, is now open for enrollment!

It covers the key distributed data management patterns including Saga, API Composition, and CQRS.

It consists of video lectures, code labs, and a weekly ask-me-anything video conference repeated in multiple timezones.

The regular price is $395/person but use coupon NPXJKULI to sign up for $95 (valid until December 25th, 2024). There are deeper discounts for buying multiple seats.

Learn more

Learn how to create a service template and microservice chassis

Take a look at my Manning LiveProject that teaches you how to develop a service template and microservice chassis.

Signup for the newsletter


BUILD microservices

Ready to start using the microservice architecture?

Consulting services

Engage Chris to create a microservices adoption roadmap and help you define your microservice architecture,


The Eventuate platform

Use the Eventuate.io platform to tackle distributed data management challenges in your microservices architecture.

Eventuate is Chris's latest startup. It makes it easy to use the Saga pattern to manage transactions and the CQRS pattern to implement queries.


Join the microservices google group