14

Just wanted to know if cyclic dependency is something that one should avoid in microservice design.

For example, let's say we have a simple web store that sells fruit.

It could have:

  • Account Service - where all information about accounts is stored
  • Order Service - where all the information about orders is stored
  • Fruit Stock Service - a simple listing of fruits, their availability is stock and prices.

Let's say that we want to forbid our users buying more than 10 bananas total. And put the info about banana availability on the screen.

So which is the better way to do it:

  1. Have Fruit Stock Service make a request to a Order Service to get all previous user orders and return it with the bananas price and stock info. In this case we have a cyclic dependency because Order Service needs to know about fruit and Fruit Stock Service needs to know about orders

  2. Have a separate request to Order Service (something like 'Can user 111 buy item 222'). In this case we have to make 2 separate request to know if we should show bananas to this user or not.

5 Answers5

13

Let's first get rid of a misconception

The microservice architecture "structures the application as a set of loosely coupled, collaborating services":

  • Collaborating mean that microservices may need to work together to achieve a higher objective. They may therefore need to know (or find out) how to work with other microservices, and perhaps even to rely on them.
  • Loosely coupled means that each microservice is well encapsulated and can be replaced by another microservice offering the same "contract", the same external interface.

The goal of this architecture is allow you to compose many simpler microservices in order to offer complex services that can be scaled. Achieving this goal does not mean that microservices are independent, but that they are that are independently deployable. And that's a big difference

Your question in theory

If a microservice A depends on a microservice B you must be able to deploy a new version of A without touching B. So the services are dependent but they can be independently deployed. As a real-life example, look at the relation between authorisation and product microservices in the Netflix stack.

If there is a cycle however, one should analyse if the service decomposition did went to far:

  • If microservice A depends on B, but at the same time B depends on A, it starts to look very much like a strong coupling. This means that something did not work as it should. Verify if both functionality should not be packaged as a single microservice.
  • If a microservice A depends on a microservice B, which depends on microservice C which depends on microservice A, it's less obvious, but the service decomposition should be challenged for the same reason.

Note that the dependency can very well be indirect (e.g. via a message queue)

Your question in practice

Let's look at the rule: "forbid our users buying more than 10 bananas total":

  • the rule is a condition for accepting the order, so it needs to be checked as part of Order Service
  • to verify the rule, you need to find all past orders of the customer (in a given time frame?) and aggregate the quantity by product. This would therefore typically be implemented as part of Order Service
  • so for your question, there is no need for questions: everything would be performed as part of one microservice.

But you could imagine less trivial scenarios:

  • Order Service could need to look for stock availability to inform the user of out-of-stock items during the ordering process. Either you'd make sure that all stock events are forwarded to order service. Or you would make Order Service addressing a request to Stock Service.
  • In B2B, it would not be uncommon that Order Service needs to do a credit check for accepting an order, since the customer would pay after having received the goods. Credit management usually combines information from lot of sources (e.g. external credit rating agencies, outstanding orders, outstanding invoices, payment history, etc...). It would be a good candidate for a microservice. But credit management requires to get invoices from accounting, which might receive them from order management. So a circular dependency is something that can happen.
Christophe
  • 81,699
1

Yes. Microservices are (by definition) independently deployable and scalable chunks of code. When you have a cyclic dependency, you can’t deploy one part of the cycle without the other. You lose the main benefit of microservices, and would be better off shipping the cyclic bits together as a larger service.

Telastyn
  • 110,259
1

A circular dependency means the design is incorrect. (Unless you are trying to model a real-world paradox). The solution is to adjust the design: { Tables, Services, Processes }.

With your example

Don't go overboard with Microservice dependencies. Every architecture has its exceptions.

I don't like Microservices architecture, but you could:

  • Add a Product table/collection
  • Add a StockArrival table/collection
  • Have a Fruit Stock Process. This would run a View View_StockLevels to derive the Stock of each Product, and store the derived value in XStockLevel. When more stock arrives or when another Order is completed, the View is rerun to update XStockLevel
  • Have a ProductService. This lets the user browse products with their associated XStocklevel
  • Change the OrderService to use the XStockLevel. If there happens to be a double-up (very rare), you can refund the customer when it comes to the fulfillment of the order.
  • It's possible to use View_StockLevels directly in the OrderService instead of XStockLevel within a locking transaction to avoid the double-up.

The full Microprocess way:

see https://colossal.gitbook.io/microprocess/differences/compared-to-microservices (I am a contributor to this draft standard)

  • Your application talks SQL directly with the database with User context, so there's no need for any Microservices - see https://colossal.gitbook.io/microprocess/definition/data-web-gateway
  • As the customer is building their order, the appropriate records may be inserted/updated on the database. Inserting the new Order record, and OrderItems with quantities. The record has a field that indicates the state is "Cart".
  • (Payment process details left out)
  • When the customer clicks to finalise the order, they INSERT into OrderCommit table.
  • An OrderCommit process sets the Commit field of the Order to true, but only in a transaction reading the View_StockLevels then setting Commit to true if there is ample.
Kind Contributor
  • 890
  • 4
  • 13
0

Cyclic dependencies between microservices should not exist because dependencies between microservices should not exist. No dependencies: no cyclical dependencies. Here's a (slightly edited) quote decent article to get an overview of the idea:

If you keep that in mind, you can develop microservices that are independent single functions, and not slide into dependency hell. Just observe two simple rules:

  • A microservice cannot call another microservice directly.
  • A microservice can be invoked in only two ways

    1. Through an event
    2. Called from a "microservice script"

What you are describing here is called a "distributed monolith". The defining characteristic of microservices that distinguishes them from web services in general is independence, or in SOA terms: autonomy.

Distributed monoliths are bad. Worse than straight monoliths. I think you have two choices here. Accept that the ordering and stock services are coupled and design and deploy them as a single unit or you go through the effort of designing these so that they do not interact directly.

The first option is perfectly acceptable, in my mind. I think one of the things that has gone off the rails with microservices is that there's an idea of absolutism where each endpoint must be its own application with its own DB. You can still get the benefits of microservices by dividing things based on their natural dependencies. Maybe 'ordering' and 'stock' are too hard to decouple but you can keep 'account' as a separate microservice. Microservices are not always the right choice. They have costs and benefits like anything else.

If you want to make this a microservice, you will need to think about how an agent or client can orchestrate calls against them and pass the necessary information from one to another. For example, you might build a reservation call in the 'stock' service. If the customer has successfully reserved the bananas and successfully ordered the bananas, you then confirm the reservation. This is going to add a lot of complexity. You need to figure out if it's worth the trouble.

I found this Munroe-esque cartoon that I think fits the situation:

enter image description here

JimmyJames supports Canada
  • 30,578
  • 3
  • 59
  • 108
0

The answers above are right but I wanted to add a bit more about how to get around this issue.

All the answers above discuss how cyclic dependencies between microservices are bad and lead to a "distributed monolith" architecture which essentially has the worst of both worlds (at least for deployment and complexity, though I guess it remains horizontally more scalable).

@Christophe's accepted solution alludes to however the fact that in more complex applications you can't really avoid cyclic usage so readily.

I faced a similar situation in my application. I had a service that (sort of) managed deals and another service that managed transactions.

when calling GET deals, it populated transaction information by calling the transactions service

However, I got a new product requirement that when creating a transaction, I also wanted to make sure it got attached to a deal.

I could have added logic in the transactions service to call the deals service to make sure the txn gets attached automatically but this would lead to a cyclic dependency.

The way around this I found was that I could use event emitters. When a transaction is created, I could emit an event that a transaction was created. Then in the deals server I could make a subscriber that watches for this event and creates a deal and links that transaction ID.

Through this pattern, the transactions microservice will never have to depend on the deals service.