Boundaries: DTOs, Leaky Contracts, and N+1
Where layers meet: boundary types that stay stable, the shape of the N+1 query problem, and seams you can grade.
DTOs: the boundary's own language
A DTO (Data Transfer Object) is a boundary type with no behavior: the shape an API or storage layer speaks. Why not expose domain types directly?
- Stability โ an internal refactor must not break external consumers.
- Security โ you choose every field that crosses; nothing leaks by accident.
- Intent โ the wire shape says what a use-case means:
PlaceOrderRequestwith only the fields that matter.
// domain owns this
public sealed class Order { public int Id; public string Customer = ""; }
// boundary speaks this
public sealed record OrderDto(int Id, string Customer);
// mapping is explicit, one direction per use-case
public static OrderDto ToDto(Order o) => new(o.Id, o.Customer);
Mapping feels like boilerplate until the first time a public contract survives an internal rewrite โ then it is insurance.
The N+1 shape (now, before a real ORM)
Fetch 20 orders, then per order fetch its customer: 1 + 20 queries. In code, the tell is a loop whose body does a lookup that could have been batched:
// N+1: one query per order
foreach (var o in orders)
o.CustomerName = _names.Find(o.CustomerId); // 20 round trips
// fixed: batch, then join in memory
var ids = orders.Select(o => o.CustomerId).Distinct();
var byId = _names.FindMany(ids);
foreach (var o in orders)
o.CustomerName = byId[o.CustomerId]; // 2 round trips
You will fix this shape against a counting fake โ the same way you will later recognize it under EF Core's lazy loading.
What "wrong direction" looks like
The classic violation: a domain type (or its method signature) referencing an
infrastructure name โ Order.LoadFrom(SqlConnection). The seam collapses:
no fake can replace the connection, so no test can run without a database.
The graded refactor, next practice, is restoring exactly that seam.