Handle Others’ Versions

When receiving requests or messages, your application has no control over the format. None, zip, zero, nada, zilch. No matter how well the service’s expectations are defined, some joker out there will pass you a bogus message. You’re lucky if the message is just missing some required fields. Right now, we’re just going to talk about how to design for version changes. (For a more thoroughly chilling discussion about interface definitions, see Integration Points.)

The same goes for calling out to other services. The other endpoint can start rejecting your requests at any time. After all, they may not observe the same safety rules we just described, so a new deployment could change the set of required parameters or apply new constraints. Always be defensive.

Let’s look at the loan application service again. As a reminder, from Table 1, Example Routes, we have some routes to collect a loan application and data about the borrower.

Now suppose a consumer sends a POST to the /applications route. The POST body represents the requester and the loan information. The details of what happens next vary depending on your language and framework. If you’re in an object-oriented language, then each of those routes connects to a method on a controller. In a functional language, they route to functions that close over some state. No matter what, the post request eventually gets dispatched to a function with some arguments. Ultimately the arguments are some kind of data objects that represent the incoming request. To what extent can we expect that the data objects have all the right information in the right fields? About all we can expect is that the fields have the right syntactic type (integer, string, date, and so on), and that’s only if we’re using an automatic mapping library. If you have to handle raw JSON, you don’t even have that guarantee. (Make sure to always wash your hands and clean your work surfaces after handling raw JSON!)

Imagine that our loan service has gotten really popular and some banks want in on the action. They’re willing to offer a better rate for borrowers with good credit, but only for loans in certain categories. (One bank in particular wants to avoid mobile homes in Tornado Alley.) So you add a couple of fields. The requester data gets a new numeric field for “creditScore.” The loan data gets a new field for “collateralCategory” and a new allowed value for the “riskAdjustments” list. Sounds good.

Here’s the bad news. A caller may send you all, some, or none of these new fields and values. In some rare cases, you might just respond with a “bad request” status and drop it. Most of the time, however, your function must be able to accept any combination of those fields. What should you do if the loan request includes the collateral category—and it says “mobile home”—but the risk adjustments list is missing? You can’t tell the bank if that thing is going to get opened up like a sardine can in the next big blow. Or what if the credit score is missing? Do you still send the application out to your financial partners? Are they going to do a credit score lookup or will they just throw an error at you?

All these questions need answers. You put some new fields in your request specification, but that doesn’t mean you can assume anyone will obey them.

A parallel problem exists with calls that your service sends out to other services. Remember that your suppliers can deploy a new version at any time, too. A request that worked just a second ago may fail now.

These problems are another reason I like the contract testing approach from Help Others Handle Your Versions. A common failing in integration tests is the desire to overspecify the call to the provider. As shown in the figure, the test does too much. It sets up a request, issues the request, then makes assertions about the response based on the data in the original request. That verifies how the end-to-end loop works right now, but it doesn’t verify that the caller correctly conforms to the contract, nor that the caller can handle any response the supplier is allowed to send. Consequently, some new release in the provider can change the response in an allowed but unexpected way, and the consumer will break.

images/handling_versions/full_duplex_testing.png

In this style of testing, it can be hard to provoke the provider into giving back error responses too. We often need to resort to special flags that mean “always throw an exception when I give you this parameter.” You just know that, sooner or later, that test code will reach production.

I prefer a style of testing that has each side check its own conformance to the specification. In the figure, we can see the usual test being split into two different parts.

images/handling_versions/half_duplex_testing.png

The first part just checks that requests are created according to the provider’s requirements. The second part checks that the caller is prepared to handle responses from the provider. Notice that neither of these parts invokes the external service. They are strictly about testing how well our code adheres to the contract. We exercised the contract test before with explicit contract tests that ensure the provider does what it claims to do. Separating the tests into these parts helps isolate breakdowns in communication. It also makes our code more robust because we no longer make unjustified assumptions about how the other party behaves.

As always, your software should remain cynical. Even if your most trusted service provider claims to do zero-downtime deployments every time, don’t forget to protect your service. Refer to Chapter 5, Stability Patterns, for self-defense techniques.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.178.133