One of our clients needed their web application to interface with two external APIs. Neither API had a native ruby gem that we could pull into the web app project. We decided to write our own gems, one for each API. The web app and the APIs all target the same domain, but don't use the same domain language. The subtle challenge was to figure out how to interface with these APIs without adding confusion to the web app's domain language. I'll talk though our story on how we came to a clean solution.
Both of the external APIs were from the same organization, but created at different times by different teams, and consequently looked a lot different from each other. What's worse, both APIs are concerned with the same core concepts and entities, but the APIs called the entities different names.
And…. it still gets worse. API-A has entities called Market and Schedule, and API-B also has an entity called Schedule. Schedule in API-B is not the Schedule in API-A, but instead the Market. That sounds confusing, because it is, so lets map that out so it's a little clearer.
API-A Market == API-B Schedule API-A Schedule != API-B Schedule
Oh, the horror!
On a positive note, our client did a very good job at cultivating the proper domain language in their application.
To summarize, here's all of the players:
- Our client's web app and its domain language
- One of the external APIs and its unique domain language
- The other external API and, yet again, its own unique domain language
Great, we have three sets of language for the same set of domain concepts. Gahh!
The inconsistencies with the external APIs are what they are, and there's nothing we can do about that. So, we made it a point to not deviate from their specific terminology and format for several reasons:
For example, if the external API had a call named "GetSchedule", then the gem's API method would look something like
- We're just wrapping their API with a nice ruby gem. It shouldn't know or care about anything else outside of itself (e.g. the web app). So, let's keep it true to what it is: a thin interface to an existing API.
- When a developer looks at one of the gem's API methods and wants more documentation, they can lookup the external API's documentation by matching up the gem's API method name to the endpoint they're interested in. This alleviates the need for a lot of redundant documentation on our end.
- It's easier to maintain a gem that exactly matches the terminology and format of the underlying API. If the external API changes or gets enhanced, it will be easy to identify the deviations and make adjustments accordingly.
Since our client's domain language was well thought-out and consistent, we wanted to do everything we could to protect it from the contrasting terminology used in the external APIs. An easy way to accomplish this was to create a new object that encapsulates the logic of an operation between the external APIs and the web app. This pattern goes by a lot of names: service, interactor, controller (no, not a rails controller), use case, and I'm sure there's more. I'll call it service for this post.
Let's take a look at an example of a service object:
The instance methods of the service object use the web app's domain terminology, and the work being done inside of the methods are making calls to the external APIs. This demonstrates a straightforward, clean translation between the domain language of the web app and the domain language of the APIs. in the above example, we see that setting the market type of the offer sets the "schedule" via API:B. Although the terminology between the two systems are inconsistent, at least we have a clear point of translation between the two. We don't have to force one system to use another system's terminology.
Here's what it looks like in diagram form
- We created gems that very closely mimic that external APIs that they are wrapping, without knowledge or care of anything external to itself (e.g. the web app).
- We created service objects that perform an operation between the web app and external APIs, allowing each side of the equation to stay true to what they are, and bridging the gap between the language differences.