:PROPERTIES: :ID: d494276b-97a5-4415-be58-20e908a84f19 :END: #+Title: Events, Circular Service Dependency, Handlers Service #+Author: Yann Esposito #+Date: [2023-12-05] - tags :: [[id:a5be1daf-1010-428f-a30f-8faf95c1a42f][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: #+begin_src method assetUpdated (newAsset): ,,, ;; do stuff foreach hook in S1.assetChangeHooks; hook(newAsset) #+end_src 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 #+begin_src method assetUpdated (newAsset): ,,, ;; do stuff pushEvent("assets/changed", {asset: newAsset}) #+end_src 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: #+begin_src S1.assetUpdated (newAsset): ,,, doStuff S2.updateAsset(newAsset) S3.updateAsset(newAsset) #+end_src or #+begin_src S1.assetUpdated (newAsset): ,,, doStuff S2.updateAsset(newAsset) ... S2.assetUpdated (newAsset) ,,, doStuff S3.updateAsset(newAsset) #+end_src 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: #+begin_src S1.assetUpdated(newAsset): ,,, doStuff handlerService.S2.updateAsset(newAsset) handlerService.S3.updateAsset(newAsset) #+end_src 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: #+begin_src S1.assetUpdated(newAsset): ,,, doStuff handlerService.S2.updateAsset(newAsset) S2.assetUpdated(newAsset): ,,, doStuff handlerService.S3.updateAsset(newAsset) #+end_src 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.