A boundary only the database crosses is not a boundary

A codebase split into Billing, Catalogue and Shipping namespaces, each with its own repositories and services, looks modular in the directory listing. Then one report joins four tables belonging to three of them, and the modules turn out to be labels on a single design that only the schema expresses.

-- this query lives in Billing and reads Catalogue and Shipping
SELECT i.id, i.total, p.name, p.supplier_id, s.carrier, s.tracking_ref
  FROM billing_invoices       i
  JOIN billing_invoice_lines  l ON l.invoice_id = i.id
  JOIN catalogue_products     p ON p.id         = l.product_id
  JOIN shipping_consignments  s ON s.order_id   = i.order_id
 WHERE i.issued_at >= '2014-10-01';

The test is not whether the code is separated but whether either side could change without the other noticing. Renaming a column in catalogue_products breaks a query inside Billing, so the boundary does not exist; what exists is a naming convention. That is frequently the right design — one well-normalised schema with joins is faster and simpler than anything that replaces it — and the mistake is believing the modules are independent and planning as though they were. If a boundary is meant to be real, the crossing has to go through the other side’s interface, $catalogue->productsById($ids), and the join disappears; at which point you discover it was doing four things the application now has to do itself, more slowly. Pick one. The failure is having the vocabulary of the first and the performance of neither.