deft/notes/events_circular_service_dependency_handlers_service.org
Yann Esposito (Yogsototh) c1d2459d0c
save
2024-08-14 11:35:42 +02:00

6.4 KiB

Events, Circular Service Dependency, Handlers Service

tags
blog
source

The Problem

Imagine you have a program that is constituted of sub-services. A service can be seen like a Singleton Object in the OOP and is a lot more natural in the Functional Programming paradigm. I feel it also has a lot better generic composability properties. Instead of dealing with thousand of similar states, you have few services, and every one of them keep their own internal state. And a full application becomes a set of services, you can decide at init which services you want to run, which you do not want, and for each service, you can have multiple different implementations so you could switch some service implementation during testing or depending on the context you are running your whole application.

Now you want to split and organize your service not necessarily by technical detail but more by functional feature. Now imagine that you have a sane organisation, every service declare the list of dependent service. The one you would like to use.

If your service dependency graph is non-cyclic this has a lot of beneficial effects. In particular for initialization order, as well as stopping order. Now imagine the following example:

AssetService -> PriceService -> BasketService

So BasketService depends on PriceService And PriceService depends on AssetService.

The AssetService internal state is about the description of assets, some might contain a price table or things a bit complex to read right away.

The PriceService uses the AssetService to retrieve the price of an asset using a potentially complex price table description from the Asset service.

The BasketService, want to show the actual price of the assets in the Basket by using the Price service.

Now, the issue. We want the state of Basket service to be updated when the asset service state change. Say the price table change for some asset.

As PriceService depends on AssetService, AssetService cannot trigger any method exposed by PriceService otherwise it would create a circular dependency. So how could we achieve the expected result?

Solutions

Refactorization

If you have Service2 that depend on Service1, but want somehow to call a method of Service2 from Service1, that is not possible. One solution is to reorganize your services.

Split Service 2, with Service2a and Service2b. Move Service2b as a dependency of both Service2a and Service1. Now Service1 know about Service2b.

That could be a solution, but it might be at the price of Buisness Logic organization. Maybe it makes sense technical to have Service2 splitted, but this is not natural in the Functional organization of your application. And thus it will make it harder to understand the organization of your system if you do so.

Hooks

You can expose a few hook methods in parent services. If you have S3 that depends on S2 that depends on S1. You can create a hooks method in S1. So during init, once S1 finished to be initialized, S2 init will be run. During the init, S2 will call: S1.addOnAssetChangeHook(S2.updatePrice).

And the same between S3 and S2.

And in S1, inside the method S1.assetUpdated you need to have something like:

method assetUpdated (newAsset):
  ,,, ;; do stuff
  foreach hook in S1.assetChangeHooks;
     hook(newAsset)

And you have to repeat this in every service that need this kind of mechanism. Which could quickly become tedious.

Events

Another option is to centralize an EventService. This is a bit similar to the hook but instead of having every service writing their own hook mechanism, you centralize this in a single service.

So if we take our previous example we will have

method assetUpdated (newAsset):
  ,,, ;; do stuff
  pushEvent("assets/changed", {asset: newAsset})

And the event service will keep track of consumer of different events and redistribute the events to the consumers. But with 3 services there could be an issue.

Say we have S1 -> S2 -> S3.

S3 uses S2, but only S1 trigger events. Imagine the following scenario:

S1 -> push asset changed event EventService -> run concurrently S2.assetUpdated and S3.assetUpdated

But S3, uses S2 to compute the basket value. The problem, S2 might not have the time to update its internal state to reflect the changes made by S1. BUG…

So here the solution is to make S2 send events after S1 updated has been handled, and S3 only react to S2 events. That will work, but.. it doesn't look very nice. Now in your code we have an issue. Instead of having something like:

S1.assetUpdated (newAsset):
 ,,, doStuff
 S2.updateAsset(newAsset)
 S3.updateAsset(newAsset)

or

S1.assetUpdated (newAsset):
 ,,, doStuff
 S2.updateAsset(newAsset)
...
S2.assetUpdated (newAsset)
 ,,, doStuff
 S3.updateAsset(newAsset)

Your business logic is hidden behind the event consumer graph. As this is done dynamically (to prevent statical circular dependency), it is a lot more difficult to think about the behaviour of your application. Mainly from S1 assetUpdated you can not discover from reading the code that this will have an impact on S2 nor S3. You could only discover that from the other way around from S3 or S2.

HandlersService

Another option is to use a messaging system. This look a lot like the event system, but this time we keep a handler service that contain a list of published handler that could be called independently of the normal service dependency graph. Here is the main idea:

S1.assetUpdated(newAsset):
   ,,, doStuff
   handlerService.S2.updateAsset(newAsset)
   handlerService.S3.updateAsset(newAsset)

now, it is visible from the code that S1 update will have an effect on S2 and S3. And you could follow the system. Unlike with events, you should run these synchronously (non concurrently). And this should greatly ease your understanding of the system.

The other option is also to:

S1.assetUpdated(newAsset):
   ,,, doStuff
   handlerService.S2.updateAsset(newAsset)

S2.assetUpdated(newAsset):
   ,,, doStuff
   handlerService.S3.updateAsset(newAsset)

But both are easier to understand than to discover that, the method create an event, and then looking in the whole code what are the services that are consumer of this specific event.