The idea: do the chatty part on the server side of the wire
A music app opens an album page. It shows the album's title, its artist and its tracks, each with its performers. On the server these are small objects, as in Martin Fowler's example in Patterns of Enterprise Application Architecture: an Album points to an Artist and to a list of Track objects, and each track points to the artists who play on it. To build the page, a client asks 16 questions (the album, its title, its artist, the artist's name, the tracks, and the title and performers of each track). The page asks those 16 questions in two ways. If every question is a remote call, the page needs 16 round trips: 812.58 ms on a link with 50 ms round trips. If a Remote Facade asks them on the server, inside one process, and sends the answers back in one Data Transfer Object (DTO), the page needs one round trip: 53.55 ms.
Each Step shows one visible change: a message crossing the wire, the facade checking the caller, the assembler visiting a domain object. The clock adds 50 ms of round trip plus the message size at 100 bytes per ms to every remote call, and 1 µs to every in-process call. The timeline keeps the last four runs on one scale, so you can compare their lengths directly.
Local and remote interfaces
A method call inside one process takes nanoseconds. A call to another process has to marshal the arguments, cross the operating system and often a network, and wait for the answer. That costs thousands of times more, even when both processes run on the same machine. On the page a local call is 1 µs and a remote one 50 ms, so the same 16 calls take 0.016 ms in one process and 812.58 ms across the wire (Demo: same objects, one process).
So an object's interface depends on where its callers are. In one process a fine-grained interface is best: getTitle(), getArtist(), small methods that are easy to combine and override. Across a process boundary you want a coarse-grained interface that does a whole job in one call. That is why Fowler's First Law of Distributed Object Design is "don't distribute your objects". To use more machines, run several copies of the whole application behind a load balancer (clustering). Each copy then makes only local calls.
Fine-grained calls across the wire
The first demo puts every domain object behind a remote interface ("distribution by class"). The client first gets a remote reference (a proxy) to the album from the registry, then calls getTitle() on it, then getArtist(), which returns another proxy whose getName() is another call, and so on down every track and performer (Demo: fine-grained remote calls). Each message is tiny: the 16 calls move only 1 258 bytes. The time goes to waiting: 16 × 50 ms. The 2-field header screen still needs 4 calls (202.94 ms), and the 60-track box set needs 365 calls, about 18.5 s.
Remote Facade: a thin skin
A Remote Facade keeps the fine-grained objects and puts a coarse-grained object in front of them at the process boundary. Here it is AlbumService, with bulk accessors such as getAlbum(id) and updateAlbum(id, dto). Each method only translates one coarse call into many fine ones (Demo: one facade call + DTO):
AlbumDTO getAlbum(String id) {
return new AlbumAssembler().writeDTO(Registry.findAlbum(id));
}
- No domain logic. Validation and rules stay in the domain objects (or in a separate, non-remote script). You should be able to run the whole application in one process without the facade.
- A natural place for the access check and the transaction. Each facade method can check the caller (the ACL box), begin a transaction, do the work and commit before it answers. A transaction should never stay open while the answer travels back to the client.
- Few facades, many short methods. One facade often serves a whole family of screens. Two methods may do nearly the same thing inside if the client sees them as different commands.
- Stateless if possible. A stateless facade can be pooled. One that keeps session state is simpler to write but costly with thousands of users.
A Session Facade (from J2EE) usually holds workflow logic, so it is a remote set of transaction scripts rather than a thin skin. A Service Layer may be coarse-grained for clarity, but it doesn't have to be remote and can return real domain objects. When the same domain model is used both in-process and remotely, the facade can sit on top of a service layer.
Data Transfer Object: the data in one block
A coarse call needs a coarse result. The domain objects themselves can't go: they form a web of references (Mai Linh is one Artist object that the album and every track point to), they carry behaviour, and the client shouldn't need the domain classes. So the server copies the data the client needs into a Data Transfer Object: AlbumDTO with a title, the artist as a string, and an array of TrackDTO, each with a title and the performers as an array of strings. A DTO holds only simple values and other DTOs, arranged as a tree. That is what makes it easy to serialize and to rebuild on the other side, where the DTO class (and only the DTO class) also exists.
<album title="Night Trains" artist="Mai Linh"> <track title="Slow Light"><performer name="Mai Linh"/><performer name="An Do"/></track> <track title="Harbor"><performer name="Mai Linh"/></track> <track title="Last Ferry"><performer name="Mai Linh"/><performer name="Hoa Tran"/></track> </album>
A DTO is usually shaped for a screen or a family of screens. You can use one DTO for a whole interaction or a different one per request, and the same or separate DTOs for requests and responses; it is a judgement call each time. The serialization can be text (XML, JSON: easy to read, bigger and slower) or binary (small and fast). Plain binary serialization breaks as soon as the two sides disagree on the class: adding one optional field on the server can stop an old client from reading anything. Field-by-name forms (XML, a map of name → value, JSON) let an old client ignore a field it doesn't know.
The assembler maps both ways
The DTO must not depend on the domain classes, because it is deployed on the client too. The domain must not depend on the DTO, because the DTO changes whenever the screen does. A separate assembler (a kind of Mapper) connects them. writeDTO(album) walks the objects and fills the DTO. createAlbum(id, dto) and updateAlbum(id, dto) go the other way. They look the artists up by name in the Registry, and they make the decisions a DTO leaves open: is an unknown name an error, or a new artist? Does a changed artist name rename the artist, or relink the album to another one? Here an unknown name is an error. updateAlbum keeps the existing album and track objects (other objects refer to them), but clears each track's performers and links them again.
Updates: one call, one transaction
To save, the client edits its copy and sends the whole DTO back in one updateAlbum call (Demo: update through the DTO). The facade checks the caller, begins a transaction, and the assembler compares and sets the title, checks the artist, and rebuilds each track's performers. That is 22 in-process calls, then a commit: 54.05 ms for the whole edit.
With a typo in a performer's name (Demo: unknown artist: rollback vs half-saved), the assembler throws halfway through, after it has already set the title. Because the whole update ran in one transaction, the facade rolls it back and the album is unchanged. With fine-grained remote setters, setTitle is its own call and is applied at once. When findArtistNamed("Ann Do") then returns null, the client stops, and the server holds a new title without the new performer: half-saved. A transaction across several remote calls would hold locks while messages travel, which is exactly what you want to avoid. Inside one process, holding a transaction around a few calls is cheap.
How big should a DTO be?
On this link one round trip (50 ms) costs as much time as sending 5 000 bytes (at 100 B/ms). That sets the break-even point. A DTO that carries more than the screen needs wastes a little time per extra byte, while a second call wastes a whole round trip. So, as Fowler advises, err on the side of sending too much (Demo: one big DTO or two small calls). For the 3-track album, the header screen gets 235 unused bytes in the full AlbumDTO (2.35 ms), but the album page is 53.55 ms with one DTO against 104.46 ms with a summary DTO plus a track list.
The rule has a limit. The box set's whole AlbumDTO is 8 478 bytes, and the header screen uses 54 of them (Demo: box set: over-fetching costs a round trip). The 8 424 unused bytes cost 84 ms, more than a whole round trip. A small AlbumSummaryDTO wins (51.27 ms against 135.44 ms), and fine-grained calls are still the slowest (202.94 ms). That is why facades often have a few DTOs per entity, each shaped for a group of screens.
Where you do have to distribute
Some process boundaries are hard to remove, and each one deserves a coarse interface:
- between users' devices and the shared server (client and server);
- between the application and the database: SQL is already a coarse remote interface, so ask for whole result sets, not one row per call;
- sometimes between the web server and the application server;
- around a vendor package that runs in its own process;
- where the application really has to be split. Keep these splits as few as you can.
Today: JSON resources, BFF and GraphQL
The names changed, the trade-off didn't (Demo: today: REST + JSON, BFF). A JSON body is a DTO, and an HTTP resource such as GET /albums/a1 is a Remote Facade method. A backend for frontend (BFF) is a facade shaped for one client, with endpoints like GET /bff/header/a1 that return just what one screen shows. A GraphQL query lets the client describe the DTO it wants, so the server builds a tailored DTO for each request. For how REST, GraphQL and gRPC compare as protocols for one screen (requests, bytes, caching, push), see REST vs GraphQL vs gRPC. An API gateway can also act as a facade at the edge by aggregating several services into one call. gRPC makes remote calls look like local ones, so the message design (one coarse request, one coarse response) still matters. In hexagonal architecture the facade is a driving adapter, and the DTOs stay at the edge, outside the core. Latency vs throughput explains where the 50 ms come from.
What the page leaves out
Transport headers, SOAP or RMI framing and connection setup (they add a fixed cost per call, which only makes fine-grained calls worse); the time the server spends reading from a database (the domain objects are already in memory); asynchronous, message-based communication, which Fowler prefers but leaves out of the book; a DTO returned as a lazy placeholder for an asynchronous call; brokers that move objects between processes; createAlbum; tracks added or removed by updateAlbum; Record Sets and dictionaries used as DTOs; generating DTOs from a description; DTO versioning in practice; caching; security beyond one access check; and stateful facades with session state.