Consider the following dependency diagram:

For those less familiar with these diagrams, solid lines with solid arrow heads indicate source code dependencies, and dotted lines with open arrows indicate types that implement a protocol.
In this little app, we have three modules:
- Weather Feature Module
- Weather API Module
- Weather Cache Module
Tightly Coupled Protocols
In the Weather Cache Module, we have a Protocol <WeatherStore> with a source code dependency on the WeatherReport type in a different module.
This presents a problem. Any type that implements the protocol will then also have a source code dependency on WeatherStore. If WeatherReport ever needs to change, we could be breaking any number of implementations of that protocol.WeatherReport
This kind of coupling can quickly become a major bottleneck. Let’s look at one way to handle this.
Data Transfer Objects to the Rescue
To decouple the protocol from the WeatherStore type, we can use a data transfer object, or a DTO.WeatherReport
Data Transfer Objects are typically simple types with no business logic. It’s just a structured data container that defines only the information that should be shared between different parts of your application.
We use Data Transfer Objects to create clear boundaries between our internal domain models and the data we expose externally.
To understand this better, let’s use one to decouple our code.
Decoupling WeatherStore With a DTO
Let’s get into the example:

This WeatherStore has an insert(:::) function that inserts an array of WeatherReport‘s into a local cache. This cache might use a variety of frameworks for persistence, such as SQLite, json files, etc. As it currently stands, each of those implementations would need to use the same WeatherReport model. This could easily cause problems. For example, the existing WeatherReport represents the data from the API response. But when we want to cache a WeatherReport, there might be other data we want to store in the model, such as a timestamp or file location. But those properties would only be relevant to a specific caching implementation.
In other words, we really have two different “WeatherReport” models:
- The API’s model
- The local cache’s model
This is the kind of problem that a Data Transfer Object was designed to solve. Let’s implement one and see how comes together.
First, we can essentially make a copy of the existing WeatherReport model, and then update the WeatherStore to reference that model instead:

Since the LocalWeatherLoader calls the updated .insert(:::) method on the WeatherStore, we need to update that as well. The cache(::) function on LocalWeatherLoader accepts an array of WeatherReport‘s and inserts them into the WeatherStore instance. Now that this instance accepts only LocalWeatherReport models, we need a way to map [WeatherStore] --> [LocalWeatherStore]:

We create a private extension on Array, scoped specifically to arrays where the elements are WeatherReport. In that extension, we define a toLocal() function that just maps the array to an array of LocalWeatherReport‘s.
Then we update the cache(::) function to use that new function:

Here’s how the change looks in our dependency diagram:

Wrap Up
Strong coupling can be hard to notice. So dependency diagrams can be a useful tool for finding couplings that can become bottlenecks.
Data Transfer Objects can be a useful tool when separate parts of your software depend on a shared model. But they aren’t always the right tool for the job. In a simple codebase, especially when it does nothing more than consume and display data from an external API, using a single shared model can work quite well.
But as soon as your app needs to do more, using Data Transfer Objects to decentralize the use of models can be a powerful tool for making your codebase more modular, more flexible, and more resilient to changes.
