All letgo backend microservices are integrated using Domain Events. In the iOS team we’ve adopted the same approach. Our UI reacts to changes that are published into an event bus which is contained in the service. This means we make the screens subscribe to model changes with minimal coupling. We‘ve implemented it with the help of the reactive programming framework RxSwift, and the concept applies to any other reactive framework.
This article discusses how to consistently refresh multiple screens that represent a model it updates. It briefly exposes some possible solutions and explains how the iOS engineering team at letgo has decided to tackle this problem: using Domain Events.
Problem: Update multiple screens with a model update
The letgo app is a secondhand marketplace. As a user, you list an item, chat with other users, arrange a meeting to sell your item and make some cash. In this process the item appears (or may appear) in many screens: main feed, chat list and conversation details, user profile… When users mark items as sold, they would expect all screens displaying that item to update with the new sold state.
Let’s imagine that I’m updating the bike listing that appears in the 3 screens
How can this problem be solved?
- Delegation pattern across screens. Problems: Tight coupling of screens and we would only have 1–1 communication.
NSNotificationCenterto notify from the service to the screens. It would make the screens loosely coupled and broadcast to all subscribers. Problem: If we are passing objects via notifications then casting is required. This results in having a weak implementation: if we change the notified types, we won’t notice when we compile since it’s not a strongly typed solution.
Have any other ideas on how to solve this? If you do, please leave a comment :)
What’s a Domain Event?
The Domain Event definition in the .NET Microservices guides:
A domain event is, logically, something that happened in a particular domain, and something you want other parts of the same domain (in-process) to be aware of and potentially react to.
The idea is that a system publishes a message (Domain Event) when something happens. The message is published asynchronously in a bus which other systems listen and react to when required. Domain Events are part of the building blocks of Domain-Driven Design (DDD).
A Domain Event is a DTO that represents the event that just happened.
Domain Events with RxSwift
The domain events solution we designed so far:
- The scope is the service. We understand the service layer as the layer exposed to the presentation layer, or other services, that uses and coordinates repositories, where the implementation of the operations happens.
- Domain events are modelled as an
enumthat describes the actions that might happen to a model instance (or its identifier)
- The events are exposed through a RxSwift
- Sometimes, we provide some helper methods to filter a given object, collection of objects or specific event types
Here’s a simple example: a
UITabBarController-based iOS app which contains 3 item lists from which we can access a screen with more details about the item. The list controller can generate random items when pressing the “+” button. The item detail controller randomly modifies the item when pressing the “Edit” button. The proof of concept is that when editing an item in a detail screen all other screens update consequently.
In the example we can see how an update makes all related screens consequently update.
ListingService defines (partially) CRUD actions interface to work with a
Listing type. These actions have some input parameters and return the result execution in a completion block. The
Result wraps either a
Listing or an error. In
events we publish the events that happened after executing the service actions. Every interested component with access to the service will be able to subscribe to the
Listing domain events and react consequently.
ListingServiceEvent enumerates the
Listing actions as events that happened with the
Listing associated type.
filteredEvents(listing:) method is just a helper method that filters the events
Observable with the given
ListingService implementation, we publish the event for each CRUD method in
eventsPublishSubject which is exposed via
ListingListViewController is the controller that shows the list of listings. It contains a
ListListView as content view.
ListingListViewController has a
ListingService injected in the controller constructor
viewDidLoad the controller subscribes to the service domain events. When an event is received the view is notified to create, update or delete the listing. Then, the view updates the
UITableView. Every time a new listing is created, updated or deleted anywhere in the app our table view is updated.
You will find a fully working example of this implementation in this repository.
A domain events approach can help us build reactive interfaces with minimal coupling. The view (or view model, presenter… depending on the presentation architecture) has a bounded context and limited responsibility as only knows about itself and the service/s domain events. Therefore, the solution improves the view testability.
Enumerations are great to model the event types and thanks to the fact that Swift is a strongly typed language, the implementation turns out to be quite maintainable.
It’s important not to forget that domain events model something that happened. Therefore using domain events to ask for actions will be a wrong usage. Also, the layer where domain event emitters are hosted should be the one in charge of the business logic for the domain types given. It’s simpler if we focus our design on the domain model.
Other possible side effects that we can trigger when receiving domain events might be:
- service dependencies: make a service react to others’ updates
- analytics / logging: track events when receiving service updates
Also, this approach can be useful as a transition to a unidirectional data flow architecture. We could consider that a domain event partially models the state update in a Redux-like architecture.
Thanks to Facundo Menzella, Jorge Avila, and Sophia Casas.