3

Consider two services (bounded contexts by DDD):

  • Sales
  • Billing

Sales is responsible for creating orders and Billing for handling payments.

Sales tracks orders and Billing holds payments:

 Sales DB                 Billing DB
+----------+-------+     +------------+----------+-------+
| order_id | paid  |     | payment_id | order_id | total |
+----------+-------+     +------------+----------+-------+
| 123      | true  |     | 456        | 123      | 789.5 |
+----------+-------+     +------------+----------+-------+

When an action is finished an event is published:

  • Sales
    • OrderPlaced
  • Billing
    • PaymentReceived

Billing collects a payment after OrderPlaced is received and Sales updates the order state when PaymentReceived comes.

This creates a cyclic dependency between Sales and Billing.

Sales(OrderPlaced) <---> Billing(PaymentReceived)

Which makesit impossible to build the services in separate artifacts (eg JARs).

The idea behind this is to have independently deployable artifacts, which can be later brought together in an application:

Application.jar (-> Sales.jar, -> Billing.jar)
WebApp.jar (-> Sales.jar, -> Billing.jar)
StandaloneApp.jar (-> Sales.jar)

A possible solution would be to create a technical cut package Events:

  • Sales
  • Billing
  • Events
    • OrderPlaced
    • PaymentReceived

Put both event classes into it and make the services depend on it:

Sales ---> Events(OrderPlaced,PaymentReceived) <--- Billing

But then I see some drawbacks:

  1. The domain events leave the domain contexts.
  2. The package Events can easily explode.
  3. Additional services depending on Events have a dependency on more than what they potentially need.

Is there a better way?

5
  • @DocBrown one issue is lack of concern separation.
    – Basilevs
    Commented Mar 11, 2020 at 9:01
  • @Basilevs: separation of concerns is not an end in itself, it is a means to an end. And I was asking the OP which real issues they are facing for this specific situation.
    – Doc Brown
    Commented Mar 11, 2020 at 9:08
  • 1
    @DocBrown The problem is the build process. I have both packages (com.example.sales and com.example.billing) in separate Maven artifacts. But those has dependencies to each other, so it is impossible to build them.
    – Barney
    Commented Mar 11, 2020 at 9:34
  • @DocBrown Thank you, I tried to collaborate more about the problem in the answer.
    – Barney
    Commented Mar 11, 2020 at 10:41
  • Make it as they where written in different languages. You could not share libs so basically you would re-write events for each boundary. Wouldn't you? Bear in mind that there's no DRY breaking in duplicating events here and there. They gather no knowledge that worth reuse. These are somewhat like messages.
    – Laiv
    Commented Mar 13, 2020 at 10:31

5 Answers 5

6

You can solve this by creating two additional packages

   SalesEvents(OrderPlaced)

   BillingEvents(PaymentReceived)

instead of one Events package. It should be clear that the cyclic dependency vanishes here, since SalesEvents and BillingEvents don't reference each other, and Sales as well as Billing both reference SalesEvents and BillingEvents, but not each other.

Now let us compare this solution to the drawbacks you listed:

  1. For example, Sales and SalesEvents belong to the same bounded context, so each corresponding Event stays in it's context.

  2. There is not one "god" package any more which is going to "explode" by getting more and more events with each new microservice.

  3. Additional services can exactly reference the events they require.

so this solves all the three issues.

This is also known as "Interface segregation principle", which can be applied not only to classes, but to packages as well.

2

A possible solution for resolving both build and runtime cycle is to re-think the API design of the services.

For example Billing can provide an API for collecting payment on request. This can be implemented via a command message CollectPayment. CollectPayment belongs to the Billing bounded context as well as PaymentReceived.

  • Sales
    • OrderPlaced
  • Billing
    • CollectPayment
    • PaymentReceived

Now, the cyclic dependency disappeared:

Sales ---> Billing(CollectPayment,PaymentReceived)

But be aware: This is a shift from choreography to orchestration which (not necessarily) tends to end up with a few too smart god services doing too much by requesting other small dumb services.

0

An "event" is something that happens at a moment in time, not something that takes action. There is one "thing" in play here which you haven't mentioned yet: an Order. Sales and Billing are two related business processes both of which update the status of an Order.

If I want to understand what can happen to an Order, I want to have to look at only one thing: that one object. I don't want to find logic scattered all over the system that is effectively based on when the changes occur.

0

You can expose two clients, SalesClient and BillingClient such that

[ Sale -> SalesClient ] [Billing -> Billing Client]

SalesClient and BillingClient are published as artifacts that others can also depend on.

Then Billing will depend on BillingClient as it has that, maybe as a submodule, as well as on SalesClient; and similar goes for Sales.

Another advantage of different clients is that whatever topic or queue details are there which should be different for different events can be contained into the client itself so that it is not duplicated across different modules.

Eg. If OrderPlaced is to be published on ORDERS_PLACED topic then that details can be put into Sales client only and other modules can depend upon sales client without duplicating the topic details.

Or even better if SalesClient has an abstracted method that hides the implementation of Queueing mechanism itself.

0

Are events the only way two services can communicate in your architecture? If not, you could use a command for one of the requests, which will reverse the direction of dependency.

For example, rather than the Billing service subscribing to the OrderPlaced event, the Sales service could directly command the Billing service to TakePayment.

In terms of dependency, this means the Sales context knows about the Billing context, but not the reverse. The Sales context knows that a payment must be taken when an order is placed, but the Billing context becomes unaware of what an order is - it is just told to take payments.

Sales(OrderPlaced) ---> Billing(TakePayment, PaymentReceived)

To keep the OrderPlaced event, you could have something within the Sales context handle that event and send the TakePayment command. This bit of code has a single responsibility of tying those concepts together.

When an order is placed, take a payment.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.