Is it "architecturally right" to have a shared common service Exposed via REST APIs called by Command/Event Handlers?

I have had a few debates on the above with Development teams, hence wish to ask this question.

The context is we are developing a “greenfield” (new platform) using the Axon framework, and every now and then there is the above question that pops up (to retrieve Read Only data or Business Rules) from a common service via APIs, from Command Handlers and Event Handlers.

Any suggestions/comments are welcome.

These are some of my thoughts:

  • Coupling: How often will the common API be changed? How stable is it? Is backward compatibility for future changes possible? How clear and focused are the functions of these shared APIs? Avoid needing to change and deploy „everything“.

  • Cohesion: Will there be a lot of calls to the Common API? Is the caller only capable of providing its functionality when the common API delivers a part of it? Then it might be that the common API shouldn’t be separate.

  • Dependency direction: Dependencies should only go in one direction. Don’t call an command handler that triggers an event that triggers an event handler that calls a common api that triggers the first command handler again. Event Handler may update their local persistence but should not produce any side effects on the command side. They also shouldn’t read the state of an command side aggregate, since you could then simply skip all the event driven overhead you hopefully initially had a reason for.

  • Testing: How hard will it become to test the system? If the common API ist robust even in test environment, then it might be ok.

If it’s just a stable, mainly read-only base functionality (audit, config,…) and it’s the easiest solution, than I wouldn’t bother that much about the word „shared“. In that case i would recommend using the entity-control-boundary pattern to have the call to the api on one place per app and referring to it via an internal boundary. The same thought many also known as hexagonal or portsˋnˋadapters. If the API changes, you should ideally only change one spot.

1 Like

Hi,

As many different factors are influencing this decision, I will try to provide a short answer.
If this really has to be a common/shared service (I would challenge this at least) then you have to query it. I would suggest to do that from Event Handlers and not from Command Handlers.

Not so good way (introduces runtime and design time coupling):

  1. Aggregate handles command in Command Handler
  2. Command Handler calls some REST endpoint to validate this decision.
  3. Aggregate publishes an event.

A better way (design-time loose coupling, scales better, performs better, testing is simple)

  1. Aggregate handles command in Command Handler
  2. Command Handler publishes an event, marking the aggregate in some pending state.
  3. Some Event Handler reacts to this pending event and calls another service (REST in your case)
  4. Event Handler will send the second command to the aggregate to mark the validation as succeeded or failed.

This event handler is an edge component (lives at the edge of the system/service), a simple adapter that translates an event from your aggregate to a REST request and a REST response to a command. Your core aggregate component is not aware of this integration anymore ;).

Please note that you can also use Axon Query Gateway to send a query rather than doing a REST call. Queries are distributed via Axon Server out of the box, so you do not need to introduce a service registry, service discovery, API gateway… (Spring Cloud Netflix, for example)

I agree with the notes John provided as well.

Best,
Ivan

1 Like

Nice discussion. I also have the same debating thoughts. In this scenarios, I am only tempted to use a BPMN based library/software. To me this scenario will evolve into an orchestrating layer and do not want to pollute Aggregates with this logic. May be Axon can intro another stereo type @Orchestrator (may be already there, sorry If I missed it) and move all these orchestrating use cases into those java class and keep aggregates separated. Please validate my thoughts.

Don’t all the goodies of the “better way” come (in this case) at the cost of introducing additinal accidential complexity into the domain code (I assume aggregate is a part of the domain code). We introduce new state of the aggreagte which does not exists in it’s business logic, it’s just made up by us so we can split something which is (from business perspective) a single operation(command) into two operations/commands. Also if the need to consult some “reference data” is a part of business logic that “better way” has just removed this logic from domain layer code (aggreagte code) into some “edge component”, and instead we put into domain layer this artifical state which does not belong to its business logic. Also since we have just introduced a new state our complexity (in domain layer, in the heart of our software) blows further we have to consider the impact of this new state on other operations/commands on the same aggregate nad also the views which are projected from this “pending” instance. State is very nasty thing in information systems. We usually do not want more state that is absolutely necessary. Not sure what is worse: introducing new state and mixing business logic with “processing logic” or coupling a command with some external service. My feeling is that the latter will bring less trouble, especially if the service is reliable.

This is probably the 5th time I come back to this discussion and reading it all from the original question to the last response. And every time I have this feeling that (perhaps because of how the question was stated) there is some weird mix of “domain design” and “implementation details” concerns.

So this time I’ll try to approach it differently. I’ll start by removing the “noise” from the original question and extract the business need. That seems to be

to retrieve read only data from somewhere

I’d argue that all the API, REST, service, … concerns are implementation details. The domain design challenge would be exactly the same if we were talking about common database or common file system.

I’d start by identifying the following

  • How important is that data for individual components? How accurate and up-to-date it must be? That will determine your data access policy. Perhaps it’s OK to request it every time, perhaps not. It may be that some components need to keep local copies (for performance or reliability reasons). It may be that some components can not or should not operate unless the data is up-to-date (whatever that means in business terms).

  • How reliable / performant the access to the data must be? How the system reacts to outages and latency? Perhaps it’s not a concern at all. Perhaps it’s so crucial that there must be more than one channel to access the data and a respective fallback strategy in place. Perhaps it differs for different components.

  • Who is the owner of that data, the access channels to it and what is the relationship with the owner of the system? In other words, what is the cost of change? Perhaps it’s the same owner and all will always evolve together. Perhaps there has to be some sort of contract between them and changes are agreed. Perhaps one side must account for any unexpected changes?

Answering those will IMHO give you a rather good idea of what the viable data access strategies are. It can be REST API, it can be DB access, it can file system, it can be any combination of the those and many more with or without caching.

As for when to access the data, that’s easy - when you need it! It could be a command handler if the command processing logic needs it. It could be event handler if it’s event processing logic that needs it. It could be service, repository, projection or any component, given that you know its expectations, assumptions, constraints and you’ve prepared the adequate data access strategy for that component.

Finally to reflect the topic question - I don’t think it’s “architecturally wrong” to call a REST API from Command/Event Handlers. It’s certainly no more wrong than executing a SQL query or reading a file from a file system. That said, all of those have consequences one should be aware of.

Just my 2 cents.