1

I'm studying DDD and trying to plan how we'll migrate from our monolith to a microservice architecture. At the moment I'm trying to work through two possibilities

The first is where the shopper is an aggregate with its own microservice. There's CRUD operations against these users and their data is replicated to other microservices where needed (i.e. Billing Address replicates to the Billing microservice)

enter image description here

The issue I see here is that if any system using this data in another microservice wants to modify their data (say update the billing address within the billing microservice) then they have to update their record & write back to the Shopper microservice as it's the source of truth. Or if they're only using a cache, they'll need to update the billing address back at the Shopper microservice and wait for their cache to refresh.

The alternative is to avoid storing data in the Shopper service at all, and only storing it in its final destination. The shopper service would act more as a gateway to call APIs to modify the shopper data in its final location (so the billing microservice is the source of truth for the shoppers billing address, and can modify that data at will)

enter image description here

This seems messier to maintain the APIs and keep track of where all the shopper data is but seems way more efficient in terms of read/write and keeps shoppers isolated to where they make sense within their bounded contexts.

Does this second approach make sense or is it flawed in some way?

2 Answers 2

2

There's CRUD operations against these users and their data is replicated to other microservices where needed (i.e. Billing Address replicates to the Billing microservice)

The main argument for data replication is that you intentionally wish to disconnect one source from another.

For example, if the customer changes their address today, you wouldn't want their orders from 2020 to suddenly show the new address. Therefore it makes sense to make a copy of the "current" address at the time of billing, and store that snapshot without it being affected by future changes made to the customer.

The issue I see here is that if any system using this data in another microservice wants to modify their data (say update the billing address within the billing microservice) then they have to update their record & write back to the Shopper microservice as it's the source of truth.

You're misusing "source of truth" here. By disconnecting the address stored with your customer and in your billing, they have become independent sources of truth, which inherently means that a change to one does not reflect on the other.

The above example of a customer moving but not wanting to update their old billings is an example of how a change in one does not get reflected in the other.

If you want a single source of truth for these addresses, then you need a single copy of that information. This could be in your Shopper microservice if the address is scoped to the customer; or it could be in your Billing microservice if the address is scoped to a single billing and therefore individually decided per billing, even if they are for the same Shopper.

If you store the data in two sources, then you by definition don't have a single source of truth. (Edit: More precisely, the concepts of a shopper's address and a billing's address are separate, with each being its own source of truth.)

Note: Supposing your Shopper becomes the single source of truth, it can still be made possible for someone to adjust the address when looking at a billing entry; but when doing so the Billing microservice should push that update to the Shopper API and not actually try to store any of that information in the Billing database itself, thus retaining the Shopper API as the single source of truth.

and wait for their cache to refresh.

Caches are a bit of an exception to the "single source of truth" design. They are not implemented for a design reason, but for a performance reason. A cache is inherently considered to be an inferior (but more often consulted) source of truth.

As the saying goes, there are only two hard things in Computer Science: cache invalidation and naming things. How you update/invalidate your cache is an entirely different topic of conversation from how to structure and design your microservices.

The alternative is to avoid storing data in the Shopper service at all, and only storing it in its final destination. The shopper service would act more as a gateway to call APIs to modify the shopper data in its final location (so the billing microservice is the source of truth for the shoppers billing address, and can modify that data at will)

Calling it "the" alternative implies that it is the only other option. It isn't.

But more than that, this approach has significant flaws. For example, let's say you update a shopper's address via the Shopper microservice, but only the Billing microservice actually stores addresses. If the shopper has multiple billings, how are you going to handle this?

  • If you would update all billing entries for that shopper, then clearly this address is scoped to the shopper, not the billing entry; and therefore should be stored in the Shopper microservice
  • If you would only update a single billing entry (specified by ID), then this should clearly have been routed via the Billing microservice to begin with, because then you're really only updating a billing address, not a shopper. The Shopper microservice should not be involved here.

The way you're suggesting going about it either proves that the address is being stored in the wrong place, or that you're going to introduce unnecessarily tight coupling between two microservices. Neither are a great idea.


It's unclear to me why you've only considered storing the address in both locations or only in the Billing database; but seemingly did not consider only storing it in the Shopper database.

I don't know the full picture of your application of course, but I see two likely courses of action here:

  • Based on some contextual knowledge I have about billing data; you generally don't want to update billing addresses from the past when the customer changes their address today. Your initial approach of storing both addresses separately is the correct approach here, but you should drop your expectation that a change in one should inherently lead to the same change in the other.
    • In cases where you need to retroactively correct a mistake in an address, it is the nature of the beast that you are then going to have to individually update the related billings (and potentially the shopper if it's their current address that needs fixing). But these situations should be rare and should not be the main driving factor of your design.
    • In cases where this kind of error correction is warranted (I've worked in similar government platforms), this significantly complicates the design of your architecture, which now needs to rely on timestamped historical logs and error-correction measures. This is not a small feat and should not be implemented when it's not that much of an issue to begin with.
  • Based on your mention of wanting to reflect the changes to a billing address in the shopper address as well, it seems like your address is considered to be scoped to the customer. If this is the case (and the previous bullet point is not the case), then it seems most appropriate to only store the address in the Shopper microservice, and the Billing microservice should fetch the address from the Shopper microservice when it needs it.

How you implement caching is a different concern, and one you should only address once you've actually settled on a design for your architecture.

4
  • Thank you SO much for the extensive response here! Your final bullet point is where a lot of my confusion comes from. Storing the address in the shopper microservice means that the billing microservices will need to pull that data by calling the shopper API (Let's assume each new bill needs to pull a default address for a shopper). Is this bad practice, since now billing seems dependent on shopper? Would we not just store the default shopper address in the billing microservice? Any changes to that default address go through the billing API Mar 2 at 18:54
  • You mentioned earlier that this depends on the scope of the billing address. So if billing address (the default billing address for a shopper) is only used in billing, would it sit in the billing microservice? If it's used for other purposes, then does it become scoped to a customer? I'm confused about what determines where it's scoped. To clarify, there'd be a default billing address per customer and a billing address per individual bill. Technically both of these could be stored in the billing database and we'd never need to make a call to the shopper API. Mar 2 at 18:56
  • @MichaelCapobianco: "Is this bad practice, since now billing seems dependent on shopper?" If you want a single source of truth, going through the Shopper microservice is the right approach by design. "So if billing address is only used in billing, would it sit in the billing microservice?" Not necessarily. Even if only used for billing, if you intend to use the same address for all billings of the same customer (forcibly so), then it is scoped to the customer, not the billing entry.
    – Flater
    Mar 3 at 11:15
  • @MichaelCapobianco: I suspect you're more looking for something along the lines of (1) shoppers and billings both store an address (2) when creating a new billing, you autofill the address with the address of the shopper, but allow the customer to adjust the address as they see fit (3) When saving the billing, you store the address that the customer gave you (whether it's the default autofill or some address they custom entered into the form). This seems to cover your bases; at least based on what I infer from your questions.
    – Flater
    Mar 3 at 11:17
1

Why do you want to migrate to microservices?

There is no "generic" way to do architectures, at least not good ones. You have to first define goals and then decide things based on those goals.

Now, in general, you want microservices because you want to scale either organizationally (different teams, iteration speed) or operationally (requests/sec), or both. Again, in general, introducing "CRUD" based microservices is a bad idea. It is easy to see why. Any "data" based interface will create dependencies between the two or even more services.

This will work if you keep it limited to a small number of services that you're only interested to scale differently runtime. For example create update and read services to the same data because they scale completely differently.

Barring any such use-cases, in general, you should strive to make microservices independent. That is, implement a complete use-case on its own, without needing to communicate with another service. This is the only way you will achieve independent release cycles or easy runtime scaling.

4
  • Independent microservice lifecycles != microservices cannot call one another. It is perfectly reasonable for one service to reference or depend on another; but their lifecycles shouldn't impact one another. I agree with not aiming to have "dumb CRUD" services and that microservices should instead be an independently defined bounded context, not just a persistence service; but there are some (perfectly valid) cases where there simply isn't much business logic other than basic CRUD operations.
    – Flater
    Mar 3 at 11:21
  • 1
    @Flater I'm struggling to imagine the use-case where it is more beneficial than not for one "microservice" to depend on another one (of course I believe this happens!). Can we even call it a "microservice" at that point? "Distributed monolith" seems more accurate. If updating one microservice can bring down the rest of them... what is the point? FWIW It seems like a modular monolith would have identical deployment considerations as the above with the benefit of static analysis and none of the extensive machinery necessary to support a "microservice" architecture. Mar 3 at 15:42
  • @king-side-slide An identity provider is a clear example here. Same goes for payment providers. An order service might want to doublecheck with the stock service that the ordered items are still in stock before registering the order. An email service might be called by another service, e.g. after placing an order. All of these can be services with independent lifecycles but they can still make use on one another in some way.
    – Flater
    Mar 4 at 19:17
  • @Flater "Identity provider" is not a good example at all. Barring some extreme use-cases there is no reason to for my service to actually contact an "identity provider". The user will make a request with a JWT, or some Token. I can validate that without contacting anyone. No dependency needed. Same for your other examples. If I had a choice in design, no dependency will always be the default, unless there is a very good reason it can't be. Mar 4 at 19:28

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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