Sure, I think perhaps my wall of text obscured the intent of the question. I believe what you’re referring to is the practice of copying down the details of a product at the time of sale, so that when viewing an order you can see what was bought at that time even if the actual Product
being referenced has changed or no longer exists. FWIW I tend to dump this kind of denormalised data into jsonb columns.
There were two parts to my questions and two parts to your answer, so I’ll provide a context and then continue with the two parts of the conversation.
The situation
Assume now that we run a company supplying jellybean vending machines to shopping centres and we have two contexts in our app: Sales
and Support
.
The Sales
context deals with our web store where we list our many offerings, take orders, process payments, track shipping to our customers and so on. We, of course, have a Product
entity and a Customer
entity within that context. A Product
will hold information about the name, price, image, description, while a Customer
will hold information about a particular customer’s name, address, email, contact name, shipping address and so on. When we create an Order
we copy in the relevant Product
information (as well as storing a foreign key too), and store a reference/foreign key to the Customer
who made the order.
We also have a support portal section of the site where people not happy with our service, or who encounter any problems can go and vent to our poor customer service people through the creation of Ticket
s. Our employees have special access to this panel where they can view all the tickets, manage them, issue refunds, respond to queries. Note that these tasks will need to access different contexts (tickets vs refunds—Support
vs Sales
) but that’s ok, since we’ve decoupled our controllers from the business logic layer!
Now, in this Support
context we clearly need Customer
s, since they are the ones creating tickets. This Support.Customer
is different from the Sales.Customer
though—we don’t need to care about their payment information, or shipping address, but in turn we might care about whether they are a VIP support client and we need to get to them right away, or we simply want to model that a Support.Customer
has_many
Ticket
s. This is still fine and in keeping with the spirit of the Bounded Context principle.
We also need Support.Product
s—again we probably don’t care about stock quantities, or pricing, but we may care about special instructions for supporting certain products, internal links to scripts or the specific set of instructions to walk a customer through getting into the back control panel of the BeanTron 3000—again, things which are relevant to us in the Support
context.
Cross-context entities at the data layer
This related to my original question. Chris made the point that you want to have two data structures, one per context, even if they “represent” the same thing, as their respective views of that thing may differ. Then, Michał made the point that Ecto allows us to define multiple schemas on the same DB table, which allows us to consolidate the storage of these different fields at the DB layer, while keeping them separate at the business logic layer. All good so far.
This is, in fact, the case with our Customer
s. Support.Customer
and Sales.Customer
are two distinct data structures in our code, but conceptually, both represent the same customer—one could say they are two views of the same data. Note however, that they will have shared fields such as name
or email
. We want to keep these fields consistent between the two views, and we want to preserve the conceptual identity of the customer—for example, if we delete one of these Customer
s, the other one gets deleted too.
Now, we can take the following approaches to storing things in the database:
-
Have two separate tables, `support_customers` and `sales_customers`, perhaps with one of them being the "canonical" or main one, storing all of the shared data as well as its context-specific data. Let's say it's the `sales_customers` table (because that was initially the only table, and we only added the support portal after our phone line started being busy 24/7).
The
Support
context, when asked to fetch a customer, actually asks theSales
context for a customer, and then strips away irrelevant fields and adds support-relevant ones, populated with data from thesupport_customers
table looked up by having asales_customer_id
foreign key in that table.We need to ensure that the Ecto schemas for the two structures are the same regarding the shared fields like
name
andemail
, and we need to ensure that when one gets deleted, so does the other (yay for DB constraints/cascades). -
Have three tables,
customers
,sales_customer
andsupport_customers
, add a new contextCustomers
, and have ourSales.Customer
s andSupport.Customer
s be created by fetching the “canonical” customer through the bounded API and then embellishing it with relevant fields from our own table. We’re going for full normalisation here, and our invariants can be enforced with DB constraints, but it seems like overkill. Again we end up creating extra contexts for anything which is shared. -
Have one table,
customers
, and haveSales.Customer
andSupport.Customer
both be an Ecto schema on the same table. This is what I believe Michał was talking about. Again we need to ensure that our shared fields are the same in the schema. But we will likely want to have shared validation logic for our shared fields for our changesets, so where does that go now?
Which one is best, or is there another, better way?
Context-switching entities
Suppose we are viewing a ticket in our support system. We have our Support.Customer
Bob, the author of the ticket, loaded available in our controller and we are displaying his name and other tickets in the sidebar. But now our support staff tell us it would really be helpful if they had a list of Bob’s recent orders in the sidebar too. To get that, we’ll need the Sales.Customer
representing Bob. How do we go about it? If we were using the “one table” method above, we could simply write
sales_customer = Sales.get_customer_by_id(support_customer.id)
but now we’ve coupled our controller to our database layer! If we change things around so that the two views of a customer no longer share an id, suddenly we are showing Jane’s orders in the sidebar and the customer support people will hate us even more.
What I propose is better, and what my question was about, is to be able to write
sales_customer = Sales.customer_from_support_customer(support_customer)
where this method will do the right thing to fetch me a different view of the same customer. If we have entities which cross contexts in our domain, they are coupled in the domain, and so I propose it’s impossible to avoid coupling in our business logic model. What we can and should do is to push that into a small codebase at the points where the two contexts interface, so that if we did change how we represent customers at the data layer, we would only need to change a few access and conversion functions in the two context modules.
Cross-context associations
This was the second part of the question. I see your point that associations should not go across contexts, because strong associations between entities usually imply they belong in the same context. What I’m referring to are “loose associations” I suppose. Let me demonstrate.
Let’s suppose that we want to be able to associate Ticket
s with a specific order, to ease things like billing or shipping enquiries. That is, we want to associate a Sales.Order
to a Support.Ticket
—a cross-context association! Now, our Support
context isn’t really going to do any processing on this order, that’s not its job. All it’s really going to do is just give us the order so we can use the functions in the Sales
context to process it appropriately.
The question is in relation to actual Ecto schemas now. Is it ok to do this?
schema "tickets" do
...
belongs_to :order, Sales.Order
...
end
Now we have signed up to validating this linked order in changesets as well I suppose, though this could delegate out to the appropriate Sales
functions.
One idea which comes to mind is to just do this:
schema "tickets" do
....
field :order_id, :id
...
end
that way, we store the relevant information, but we don’t pretend to know anything about its domain.
— Hey, Support
, what’s the date of the order associated with this ticket?
— I don’t know what orders are, or if they have dates, or where they’re stored, but hey, here’s this arbitrary id
that Sales
gave me to hold on to, why don’t you go ask her about it?
— Oh ok, thanks!
Thoughts?