Pub sub system pros and cons

This is part of the Semicolon&Sons Code Diary - consisting of lessons learned on the job. You're in the architecture category.

Last Updated: 2024-04-19

Abstract description of pub/sub event systems

There are at least three components: - an event: this is a data container holding information related to the event. E.g. OrderShipper will contain the $order. - a listener: this performs actions necessary to respond to the event. - some some of message bus to connect the events to the listeners.

There is probably also a configuration file with a dictionary mapping event types to arrays of listeners.

How might this design affect coupling?

Coupling is direct knowledge one component has of another. Tight coupling is a pointer directly to a concrete class that provides the required behavior. Loose coupling is a pointer to an interface of some description.

Imagine this (in ruby, not PHP mind you)

class Post
  after_create :create_feed, :notify_followers

  def create_feed
    Feed.create!(self)
  end

  def notify_followers
    User::NotifyFollowers.call(self)
  end
end

class PostsController
  def create
    @post = Post.build(params)
    @post.save
  end
end

gets rewritten as:

class Post
end

class PostsController
  def create
    @post = Post.build(params)
    if @post.save
      publish(:post_create, @post)
    end
  end
end

class FeedListener
  def post_create(post)
    Feed.create!(post)
  end
end

class NotifyFollowersListener
  def post_create(user)
    NotifyFollowers.call(user)
    # You can add more actions here too, if you like.
  end
end

What are the consequences of this rewrite?

Classic use cases

1. Components in enterprise software that should not be changed

e.g. because they are considered working, or you don't want to incur a round of QA, or, the code truly belongs elsewhere, or their team are hostile to you going in and making changes.

Here, if their code simply publishes an event whenever relevant, you can hook your code into (or swap out, accordingly) without having to mess with their code.

2. You do not want to introduce dependencies into the code doing the calling

I guess the obvious case of dependencies to avoid are binaries that need to be installed on a system. But even within a single binary program, it could be some dependency that is difficult to test, or that is slow/memory heavy to initialize or pass around, or which breaks the layering.

3. When designing libraries that will be used by people without access to the library's source code (or with it, but no interest in modifying it)

Your documentation states that specific events are raised under specific circumstances. They can then, in turn, subscribe and respond to those events.

Example: Client-side JavaScript hooking into browser events triggered by the user.

const el = document.getElementById("close_popup")
el.addEventListener("click", (event) => hide(event.target))

The browser code might look like publish("click", thisElement)

4. Async performance smoothing

Allow you to throttle heavy load periods (e.g. if Obama tweets a lot of work needs to be done and it would overload the system by using all RAM then going to super slow virtual memory in dealing with this). Better to release in manageable batches with a queue worker.

Difference between Pub/Sub and the Observer pattern

Firstly, their coupling differs:

Therefore with pub-sub we can communicate messages between different system components without these components knowing anything about each other's identity.

Secondly, although not mandatory, observers are usually synchronous, pub/sub is usually async (message queue).

Thirdly, the observer pattern is implemented in a single application, whereas pub/sub may be cross-application.

Advantages of pub/sub event systems

Issues

Tips

Resources