Distribution and the Remote Facade / DTO
Distribution and the Remote Facade / DTO#
TL;DR#
Fowler's "First Law of Distributed Objects" — don't distribute your objects if you can possibly avoid it — sets up this closing lesson's central tension: a fine-grained object model (many small method calls, natural within one process) becomes catastrophically slow across a network boundary, where each call pays a real, fixed latency cost. Remote Facade wraps a fine-grained object model behind a small number of coarse-grained methods designed specifically for remote calling; Data Transfer Objects (DTOs) carry data across that boundary as plain, serializable structures, decoupling what crosses the wire from the domain model's actual internal shape.
The idea#
Everything in this subject up to this point (Domain Model's rich object graphs, Data Mapper's careful translation, Unit of Work's batched commits) was designed and reasoned about assuming calls between objects are cheap — a normal, in-process method call costs nanoseconds. The moment any of these objects need to be called across a network boundary (a separate service, a separate tier), that assumption catastrophically breaks: a single network round trip costs milliseconds at best — many orders of magnitude slower than an in-process call — and a design built assuming cheap calls, if distributed naively, makes many small remote calls where it previously made many small in-process ones, multiplying that per-call cost disastrously.
How it works#
The First Law, and why fine-grained objects fail across a boundary#
Worked example of the failure mode. A Customer domain object with fine-grained getters (getName(), getCreditLimit(), getAddress(), getPhoneNumber(), ...) works fine in-process — a caller invokes four getters, paying four nanosecond-scale costs. Distributed naively (each getter becoming its own remote call), the same four calls now pay four millisecond-scale network round trips — a slowdown of roughly six orders of magnitude for the exact same logical operation, purely because of where the object boundary happened to be drawn relative to the network.
Remote Facade — coarse-grained methods specifically for the boundary#
Rather than exposing the fine-grained domain model directly across the network, wrap it behind a Remote Facade: a small number of methods, each doing significantly more work per call, specifically designed to minimize the number of round trips needed to accomplish a typical remote client's actual task — directly design-patterns/07's Facade pattern, specialized specifically for the network-distribution problem.
Worked example.
# Fine-grained domain model (fine for in-process use, catastrophic if each method is a remote call)
class Customer:
def get_name(self): ...
def get_credit_limit(self): ...
def get_address(self): ...
# Remote Facade — ONE coarse-grained call replaces what would have been many fine-grained remote calls
class CustomerRemoteFacade:
def get_customer_details(self, customer_id) -> CustomerDTO:
customer = customer_repository.find(customer_id)
return CustomerDTO(
name=customer.get_name(),
credit_limit=customer.get_credit_limit(),
address=customer.get_address(),
) # all the fine-grained in-process calls happen HERE, server-side, cheaply
# the CLIENT makes exactly ONE remote call and gets everything it needs
The fine-grained Customer object's getters are still called — but all of them happen server-side, in-process, where they're cheap; the remote client makes exactly one call and receives everything it needs in one round trip, rather than paying the network cost for each individual fine-grained interaction.
Data Transfer Object (DTO) — plain, serializable data crossing the boundary#
A DTO is a simple, behavior-free structure (echoing clean-code/06's data-structure style, deliberately) whose only job is carrying data across a process/network boundary — never the domain object itself, which would either fail to serialize cleanly (circular references, database connections held internally) or, if it did serialize, would leak the domain model's internal shape and any future refactoring of that shape directly into the wire format every remote client depends on.
Why not just serialize the domain object directly? Directly connecting to clean-architecture/08's "cross boundaries with plain data, not Entities" guidance: if Customer itself were serialized and sent across the wire, any future change to Customer's internal structure (a renamed field, a new computed property, a changed relationship) would risk breaking every remote client depending on that exact serialized shape — DTOs decouple the wire format from the domain model's internal structure, letting each evolve independently, with the Remote Facade responsible for the (potentially nontrivial) translation between them.
The trade-off — and why this pattern combination is worth its cost specifically at a network boundary#
Both Remote Facade and DTO add real translation code and an extra layer of indirection that would be pure, unjustified overhead for in-process object interaction (echoing this whole subject's repeated proportionality theme) — but at a genuine network boundary, the cost they impose is dramatically smaller than the cost of not using them (the six-orders-of-magnitude slowdown from naive fine-grained distribution shown above). This is a specific, sharp instance of clean-architecture/09's cost/benefit boundary-drawing test: the "boundary" here (a network call) has enormous, measurable, unavoidable cost per crossing, which straightforwardly justifies paying the coarse-graining and DTO-translation cost to minimize how many times that boundary gets crossed.
Pros#
- Remote Facade minimizes the number of expensive network round trips a remote client needs to accomplish a given task, directly addressing distribution's fundamental cost asymmetry.
- DTOs decouple the wire format from the domain model's internal structure, letting each evolve independently without breaking remote clients on every domain refactoring.
- Applying both patterns specifically and only at genuine network boundaries (not throughout an entire in-process codebase) keeps their real overhead proportionate to where it's actually needed.
Cons#
- Writing and maintaining Remote Facade and DTO translation code is real, ongoing effort — every new piece of data or capability a remote client needs requires deliberate design of the coarse-grained method and its DTO shape, not just an automatic pass-through.
- Coarse-grained Remote Facade methods can end up returning more data than every specific remote client actually needs for its specific purpose, trading some bandwidth/over-fetching cost for the reduced round-trip count (usually still a favorable trade, but a real, non-zero cost).
- Applying Remote Facade/DTO patterns to in-process object interaction (where no real network boundary exists) is pure unjustified overhead — a common mistake when teams cargo-cult these patterns without recognizing they're specifically a network-boundary technique.
Alternatives#
- GraphQL — a more flexible alternative to a fixed set of coarse-grained Remote Facade methods, letting remote clients specify exactly what data shape they need in a single request, addressing some of Remote Facade's over-fetching concern at the cost of more complex server-side query resolution.
- gRPC and Protocol Buffers — a more modern, more efficient (binary, strongly-typed) alternative to hand-rolled DTOs and JSON/XML serialization, achieving the same underlying "plain data crossing a boundary" goal with better performance and stronger typing.
- Fine-grained distributed objects with client-side caching/batching — attempts to mitigate (rather than eliminate) the fine-grained distribution problem by batching multiple fine-grained calls together at the client, a more complex alternative to simply designing coarse-grained server-side methods in the first place.
When to use it#
Apply Remote Facade and DTOs specifically and only at genuine network/process boundaries — between a client application and a remote service, between separate microservices (see architecture/microservices-patterns), or any point where a real, measurable network round-trip cost exists.
When NOT to use it#
Don't apply Remote Facade/DTO patterns to in-process object interaction within a single application/process — that's paying real translation overhead for a network cost that doesn't actually exist there, echoing Fowler's First Law directly: don't distribute (or design as if you're distributing) what doesn't need to be distributed in the first place.
Key takeaways / mental model#
Before designing any cross-process interface, ask: "is there a real network boundary here, and if so, how many round trips would a typical client task require with a fine-grained interface?" If the answer reveals many small calls across a genuine network boundary, that's exactly when Remote Facade's coarse-graining and DTOs' plain, decoupled data shape earn their real, but proportionate, cost.
Self-check questions#
- Using the
Customergetters example, calculate (roughly) the relative cost difference between four in-process calls and four naive remote calls, and explain what Remote Facade does to avoid paying that cost. - Why shouldn't a domain object like
Customerbe serialized and sent directly across a network boundary, instead of using a DTO? What specific future problem does the DTO's decoupling prevent? - Describe a case where applying Remote Facade/DTO patterns to in-process code (with no real network boundary) would be unjustified overhead, echoing Fowler's First Law.
- Given a specific remote client's actual needs, sketch what a well-designed coarse-grained Remote Facade method and its corresponding DTO would look like, and explain how many round trips it saves compared to a fine-grained alternative.
References#
- Patterns of Enterprise Application Architecture (Martin Fowler), Chapter 5: "Concurrency" (Distribution Strategies) and Chapter 15: "Distribution Patterns" (Remote Facade, Data Transfer Object).
- See also:
architecture/microservices-patternsandarchitecture/building-microservicesfor the fuller architectural treatment of distribution this pattern anchors.