6

We are developing an application where providers can offer their products and consumers can buy them (sort of marketplace). We try to apply DDD concepts into our model design and the implementation follows a microservices style. This implies that the data belongs to a Bounded Context (BC) and only the microservices within that BC can access it. Outside that BC, specific information can only be either queried through a public interface of the BC or by subscribing to events published by that BC.

My question is about the design of the Orders. Orders are placed by consumers and accepted and fulfilled by providers. They can also be manipulated by customer service. An order right now contains only products from a single provider, but I might be asked in the future to support buying from multiple providers at once.

All implementations I've seen of similar systems contain a single Order model, which tends to be really bloated with information about the products, the provider, the consumer, invoicing, deliveries, payments, etc. I am trying to avoid that, but I am facing the question of "Who owns the order"?

I can think of the following answers:

  1. There is an Orders bounded context which is accessed by both the consumer and the provider. This means that the consumer API has a Place Order operation that talks to the Orders BC and creates an order and the Providers API has an operation like Accept Order which talks to the same Orders BC and changes the status of that same order model.
  2. There are 2 Orders BCs: Consumer Orders and Provider Orders. The Consumer API places an order in the Consumer BC. This creates the order and publishes a ConsumerOrderCreatedEvent. The ProviderOrders BC listens to that event an creates a local Order (ProviderOrder) which references the ConsumerOrder. Through the Provider API, the order can be accepted, which will publish a ProviderOrderAcceptedEvent, which will allow the ConsumerOrders to mark the order as accepted and notify the consumer about it.

My personal preferred approach is option 2 as I can see several benefits (see below), but I'm not sure if they are worth the added complexity.

I can't formulate a specific question, but as this problem must have been solved thousands of times, I'd like to know if there is one preferred approach, well-known solution or reference design that can help me.

Benefits of separate ProviderOrders and ConsumerOrders bounded contexts:

  1. A single ConsumerOrder can generate multiple ProviderOrders (if the order contains products from multiple providers
  2. The workflow of a ProviderOrder might be different/more complex than the workflow of a ConsumerOrder.
  3. Both the consumer and the provider need to see their order history, which I envision as a denormalized table for fast reads, but both order histories contain different data (ie consumer orders contain provider information and provider orders contain consumer information) and are queried differently (by the consumer and by provider). This can be implemented in single table obviously, but it seems cleaner if they are 2 tables dedicated to a single purpose.
  4. Data isolation/partitioning. Consumer orders are always accessed by consumer Id, Provider Orders are always accessed by ProviderId.

I'm having a very interesting conversation about this topic on a separate forum, so I thought I should link it here, in case someone wants to read more thoughts on this topic. Same question on NServiceBus discussion board

Note: This is implemented in .NET, by multiple teams, from multiple repositories and Visual Studio Solutions, hosted in a Service Fabric cluster and using NServiceBus for messaging.

9
  • What are the "several benefits" that you see? Feb 19, 2018 at 18:20
  • How do you envision the providers interact with your architecture? I would imagine that each provider would also maintain their orders. With that you have multiple places to find orders: the consumer, your application, and the providers.
    – Erik Eidt
    Feb 19, 2018 at 18:25
  • @RobertHarvey see my update. Feb 20, 2018 at 8:16
  • 1
    @RobertHarvey probably just fear of introducing too much complexity: It means both types of orders will be synchronised by events. They'd be nicely decoupled, but it makes it hard to follow what is going on just looking at the code as you need to track all event subscribers. Potential event versioning issues. Need of monitoring the queues' lengths, etc. Feb 20, 2018 at 16:53
  • 2
    Yes. This is exactly the sort of analysis you should be doing. Now you just have to decide. :) It's not about the "standard" way of doing it; it's about which approach is best for your specific situation, given the pros and cons. Feb 20, 2018 at 17:00

3 Answers 3

10

The two-context approach reduces complexity by segregating your core into multiple contexts each specifically responsible for carrying out and enforcing rules. It also makes your model more expressive.

It is a common misconception that Domain objects should be based on some physical "thing" (i.e Customer, Order, Provider). We have to remember that the goal of DDD is to model the behavior of a system such that the resulting model represents useful abstractions based on the functional requirements and core business logic. And because the data of real-world objects rarely provides a good starting point for modelling functional requirements, domain objects are more likely to be named based on behavior in which they will engage rather than data/attributes they contain.

Many projects start with the physical model at the forefront of the modelling process which often leads to objects with too much responsibility and overlapping concerns (ultimately manifesting in your problem above with confusion over who "owns" what). Terms like Customer, Order, and Product (aside from not implying any behavior) tend to be too abstract, encompass too much knowledge, and therefore probably aren't very helpful in representing business logic.

With the above in mind, let's take a look at some possible domain objects we might find in a few different contexts:

Shopping

  • Shopper
  • ShoppingCart
  • CartItem
  • Coupon
  • Brand

Billing

  • Cashier
  • Buyer
  • Seller
  • PurchaseOrder
  • LineItem
  • PaymentMethod
  • BuyerInvoice
  • SalesReceipt
  • Currency
  • Money

Logistics

  • Manufacturer
  • Consumer
  • PurchaseRequest
  • Product
  • ProductType

Notice that this breaks down the "Customer" into more specific roles better suited to emphasize behavior (Shopper, Buyer, Consumer). Other contexts may partition a "Customer" even more (Visitor, User, Reviewer, etc). The same applies to "Order" (ShoppingCart, PurchaseOrder, PurchaseRequest), "OrderItem" (CartItem, LineItem, Product), and "Provider" (Brand, Seller, Manufacturer).

Although many of these objects will map to the same physical model, the data is broken down and managed according to different logical models. For example, we may want to know the IP address of Shoppers, the transaction history of Buyers, and the address of Consumers (or metadata like income for analysis).

I hope this points you in the right direction.

9
  • This is another awesome answer! So how do you deal with a shopper checking out and then becoming a buyer if they're in different BCs. Basically what I'm getting at is BC inter-communication.
    – keelerjr12
    Dec 4, 2018 at 22:45
  • Generally speaking, inter-communication is done via integration events (application-layer). Your specific question doesn't really require communicate of this sort. A Shopper needn't become a Buyer, a Shopper is already Buyer. These two concepts are not distinguished by their identity (they may share an id), rather their behavior. Checking out requires reading the ShoppingCart from the Shopper's session (assuming web-based), and passing the shopperId and products to the appropriate command. The Shopping context doesn't need to talk to the Ordering context directly. Dec 5, 2018 at 16:55
  • WRT to DDD we hardly ever speak of the persistence model; however, would a shopper AND buyer map to the same table for some properties? Or would they be completely distinct. The concepts I understand; it's the implementation details that escape me.
    – keelerjr12
    Dec 5, 2018 at 17:03
  • @keelerjr12 How these entities map to a data store is indeed an implementation detail. For example, the Shopping context in it's simplest form might not even exist in our domain, rather, it might exist in our UI and persisted to a session on a web server. Of course in the real world load balancers exist and/or our business wants to collect user data. This may require an RDBMS solution (and the movement of this context into our domain). At the very least I would expect a Shopper to somehow relate to a Person. This means, at a minimum, we will have a FK. So completely distinct? No. Dec 5, 2018 at 18:42
  • Awesome! Final question, where would the mapping happen to go from CartItem to LineItem in a PurchaseOrder? For example, when the Buyer.PlacesOrder(List<LineItem> items)? I obviously need to map the Shopping Context's ShoppingCart's CartItems to LineItems? Would this be the Translation Layer or Anti-Corruption Layer that maps between different BCs?
    – keelerjr12
    Dec 5, 2018 at 18:54
2

I assume you are using a microservices architecture to decouple your application into independently operating/scaling/developable/deployable parts.

It seems to me, that "ProviderOrders" and "ConsumerOrders" are not independent in this sense. They are interdependent. They need to communicate in both directions, they need eachother to fulfill their own responsibilities, they have a circular dependency. Eventing would cover this up to some extent, but it's still there.

So this would be an argument against option 2.

Now, you can still keep both concepts and all the benefits of that, and put it into a single service. Let's call that version 1B. You eliminate the conceptual circular dependency and the communication overhead.

3
  • Yes, this circular dependency exists. Lately I am thinking that even if both concepts exist, they wouldn't be in two different Bounded Contexts, but both would belong to a single Sales BC, which I think it's what you propose. Now, if I avoid events and eventual consistency, I go to the other side: transactionability. Theoretically, I shouldn't modify 2 different aggregates (both types of orders) in a single transaction, but in practice, that could avoid weird scenarios where one modification works and the other fails. Or would you put both concepts in the same aggregate? Feb 21, 2018 at 14:42
  • I don't know enough about your use-case to make that decision. I would suggest you forget the technicalities for a moment, and list all the (business) functionality you want to cover. Assign those to concepts you and the business share. In other words do OO design. If you do that right (admittedly not always easy) you will get methods for every business use-case. I.e. methods that will be exactly one transaction. You are trying to do it the other way around, trying to guess in advance what should be an aggregate. Feb 21, 2018 at 15:59
  • Sure, I have many concepts, use cases and ideas in my head and it's time to start formalising them into more specific requirements and solutions. Thanks. Feb 21, 2018 at 16:08
1

I think it's a concierge service with multi vendor. You won't be able to come to conclusion in this way.

There are many dependencies among the Consumers and Providers. Actually no provider owns an order because an order may logically contain many providers' products but physically requests a single particular product, i.e. an order from a consumer may have 3 products from 3 different providers. So each provider gets a single order from that consumer's order.

You need to analyse the order physically rather from the point of a consumer.

2
  • Not sure if I follow 100%, but it seems you are in favour of my option number 2. Having separate ProviderOrder and ConsumerOrder, where a single ConsumerOrder might relate to several ProviderOrders, right? Feb 21, 2018 at 8:32
  • Correct. A ConsumerOrder have many ProviderOrder.
    – kazi
    Feb 22, 2018 at 8:40

Your Answer

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

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