No model is universal. Billing’s “Customer” and marketing’s “Customer” are different types; merging them produces a god table with 60 nullable columns.
Where the problem comes from. In an online store four departments say “order”, and each means something different: the warehouse means line items and shelf locations, accounting means the amount, tax and payment status, shipping means the address and box dimensions, support means the conversation with the customer. As long as this is a single Order class, it has to carry all four departments’ fields at once, and every consumer leaves three quarters of them empty — which is exactly how you end up with a table of sixty nullable columns. Worse than the columns is what gets glued together: the departments have independent reasons to change (1), and a change to the shipment format now passes through a type that tax calculation depends on.
What a boundary is. A bounded context is a region of the system inside which every word means exactly one thing, and on the way out of which meaning has to be translated. Billing’s “Customer” and marketing’s “Customer” are not two versions of one concept but two different concepts that happen to share a word: a river bank and a savings bank do not become one thing because their names match. Matching fields prove nothing — both have id, email, name, but they will change for different reasons and on different days. The criterion is the same as Parnas’s (12): the boundary follows a decision that gets revisited separately — and for the same reason DRY (43) does not apply across it.
// Different crates, different types; translation at the boundary is explicit
// crates/billing/src/lib.rs
pub struct Customer { pub id: CustomerId, pub payment_methods: Vec<PaymentMethod> }
// crates/crm/src/lib.rs
pub struct Customer { pub id: CustomerId, pub segments: Vec<Segment> }
// Anti-corruption layer: no From<crm::Customer> on purpose — mapping only through
// an explicit function carrying business decisions, not a mechanical oneTranslation is a business decision, not field shuffling. The layer that performs it is called an anti-corruption layer: it exists so that another context’s concepts do not leak in. The absence of From<crm::Customer> in the example is deliberate — an automatic conversion pretends the types are the same, and the very first question, “does marketing consider a customer blocked for debt to be active?”, shows that they are not. Such a question is answered not by a mapper but by whoever owns the context.
Three mistakes at the door. A bounded context is not a microservice: boundaries of meaning are drawn in code, and four contexts live perfectly well in one process and one codebase; splitting them into services is a separate decision with a separate price (13). An identical set of fields is not proof of a single type but today’s coincidence, and the axis of change has not been established yet (42). A convenient Customer.from(...) is the most expensive of the three: it is not forbidden, but it creates the impression that translation is mechanical, and thereby hides the place where a decision had to be made.