12

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.

James Youngman
  • 3,104
  • 2
  • 14
  • 19
MicroMaster
  • 137
  • 1
  • 4
  • What service manages the rule limiting to 10? Presumably, there can be lots of rules. – Erik Eidt Sep 16 '19 at 15:13
  • In this example it will be Order Service. Because you can't place an order if you already bought 10 bananas. – MicroMaster Sep 16 '19 at 15:21
  • I don't understand this example. "Order Service" will surely need some knowledge about the items involved in orders (like fruits), independently of other existing services. So how does this cause a dependency on the "Fruit Stock **Service**"? – Doc Brown Sep 16 '19 at 15:31
  • In the first case scenario We want to make request GetBananas to Fruit Stock Service and we want a response that will show us if the item is available. So, if we want to show a badge "Bananas are not available for your because you have already bought 10" Fruit Stock Service should make a request to Order Service, get all the orders and see if the user already have bought a fruit. So we get a cyclic dependency. – MicroMaster Sep 16 '19 at 15:36
  • If there are limitations of what a user can order based on certain products, or order history, then those can be enforced without any other services. Apply rules for SCUs directly in the shopping cart. If you need previous order history, then you only need to look at the history of orders the user made (from the same service). – Berin Loritsch Sep 16 '19 at 17:06
  • Surely this is an orders problem *only*? A user cannot order more than 10 bananas. Whether you actually have 10 bananas is completely irrelevant. The rule applies just as well to back-orders if you don't have them in stock. – user253751 Sep 15 '20 at 19:09

5 Answers5

11

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.
ThinkBonobo
  • 345
  • 3
  • 10
Christophe
  • 74,672
  • 10
  • 115
  • 187
  • How about OPs "And put the info about banana availability on the screen."? Does the OrderService facilitate that? – Kind Contributor Jan 02 '21 at 01:06
  • @Todd Just do it this way: https://microservices.io/patterns/ui/client-side-ui-composition.html - UI constraints shouldn’t define the architecture, should they? – Christophe Jan 02 '21 at 11:32
  • OP was asking about the service-side: what service would such a UI component use? You forbid via the `OrderServer`; but for completeness, you should include where to GetStockLevel from as well. Maybe `FruitStockService`? – Kind Contributor Jan 02 '21 at 12:53
  • 1
    @Todd Thanks for the suggestion ! – Christophe Jan 02 '21 at 14:32
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
  • 108,850
  • 29
  • 239
  • 365
  • You don't really lose all of the benefit of microservices, you can still upgrade components individually. I would strong reevaluate a cyclic dependency as a potential red flag for bad design,but I'm not so sure you can outright dismiss it from ever being a valid use case. Separating the two microservices even with a cyclic dependency still allows for individual scaling (both up and out) and redeployment, which can be enough of a reason to use microservices. – Flater Sep 16 '19 at 16:26
  • Note that when _every_ feature of A depends on B, my comment is less applicable. But if A depends on B only for a chunk of its features, you may want to scale A separately from B depending on how often the B-dependent features are used vs how often the not-B-dependent features are used – Flater Sep 16 '19 at 16:27
  • 1
    @Flater - not in my experience. If you update A first, it breaks since B is old. If you update B first, it breaks since A is old. You have no path to evolve your services. – Telastyn Sep 16 '19 at 18:48
  • You don't version your services? – Flater Sep 16 '19 at 19:00
  • @Flater - sure, same problem applies. Rev A to v2, B isn't yet on v2, A v2's calls don't work. Probably triggers CI failures in the process. – Telastyn Sep 16 '19 at 19:25
  • This answer does not really contribute to the question asked - because the OPs interpretation of "dependent" is probably a different one than the one used in this answer. – Doc Brown Sep 17 '19 at 05:33
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
  • 802
  • 4
  • 12
  • At first view, this seems to be a completely different paradigm. Two questions: if the application talks directly to the database, doesn’t this mean that all the application need to know the implementation details of the order and the product and that this will loose encapsulation? Doesn’t the database become the new monolith in such an approach? – Christophe Jan 02 '21 at 11:26
  • @Christophe Only the last part of my answer is a new paradigm. i) the application either needs to know the Orchestration API with Microservice or the DB Views with direct database connection - they are both contracts with the application; no encapsulation is lost; ii) "Monolith" is just a cuss word. A view can be deployed independently of other views. How that is managed in DevOps is variable, but can certainly be managed and deployed per-View (Orchestration). The "compared-to-microservices" link does cover all of this. – Kind Contributor Jan 02 '21 at 12:58
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
  • 24,682
  • 2
  • 50
  • 92
  • I really don't understand the article, could you please explain what the alternatives to having microservices call each other? I understand you wouldn't want strong coupling (the microservices should couple to the interfaces of the data/request/response payloads, but not each other, explicitly), but idk how you prevent them from calling each other. – Alexander Sep 15 '20 at 20:53
  • I don't have a lot of time but I can give you my go-to example for the 'perfect' microservice. It's the document service. You have some sort of binary file e.g. a picture but it could be anything. You can upload a file and get a new URI for it. Now, that URI refers to that document. You have another service that allows you to search for things. What does it return? The URIs from your document service. The client flow is that you call the search service and get a list of URIs. Your client then retrieves the URIs as needed. The search service never calls the document service. – JimmyJames Sep 16 '20 at 21:21
  • That was a little rushed but let me know if it helps or if you need me to elaborate. A term (that I think is the worst acronym/initialism ever) that you can search for is HATEOAS. Like I said (in comicbook store guy voice): worst acronynm ever. – JimmyJames Sep 16 '20 at 21:23
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.

ThinkBonobo
  • 345
  • 3
  • 10