Recently in my current project working for the Passport Office, I needed to make a change that depended on the state of an interview associated with a passport application. This entailed making a call to a service, and the service contract looked something like this:
GET /interview/{applicationId}/state{"state": "NOT_BOOKED"}
Very simple. Accordingly, the data structure to receive the response looked like this:
public record InterviewStateResponse(String state) { }
my client code looked something like this:
public InterviewStateResponse getInterviewState(UUID applicationId) {return interviewStateResponse = restClient.get().uri("/interview/{applicationId}/state", applicationId).retrieve().body(InterviewStateResponse.class);}
and the usage of it looked like this:
var interviewStateResponse = client.getInterviewState(applicationId);if ("NOT_BOOKED".equals(interviewStateResponse.state()) {// do whatever if the interview not yet booked}
So far, so good. But also, so much for static typing. You might be thinking: Rich, why not create an enumeration for that interview state? Then you wouldn't have that awkward literal "magic" constant in the code. There is a reason: enums are a bit of a liability in service contracts. Using one will cause it to break whenever the downstream service returns an interview state not represented in my enum. This would couple the two services such that my upstream service would have to be deployed whenever a new interview state was added to the downstream services. Independent deployability is, of course, one of the justifications for creating a service-oriented architecture. Also, the main purpose of static typing is to catch these kinds of mistakes at compile time, but the compiler cannot help me here. (Contract testing might help, but it still lacks the immediacy of static typing, and in any case we don't have contract tests and I wasn't in a position to create them).
Of course, it cannot be avoided that the two services are coupled. What I wanted to achieve was a middle ground where I have to modify my upstream service only when a new interview state is added that I need to know about—because it needs specific handling—but otherwise unexpected interview states are accepted gracefully without modification to my service. Therefore I had to keep the string in the service contract, but I still wanted to use static typing internally in my service as much as possible.
The way I did this was to define an enum containing the interview states my service knows about—not necessarily the full set in the downstream service—and I put it inside the data structure:
public record InterviewStateResponse(String state) {public enum InterviewState { NOT_BOOKED, PENDING, IN_PROGRESS, COMPLETED }}
Then I delegated the interview state comparison to a method inside the structure:
public record InterviewStateResponse(String state) {
public boolean stateIs(InterviewState compareWith) {return state.equals(compareWith.name());}
public enum InterviewState { NOT_BOOKED, PENDING, IN_PROGRESS, COMPLETED }}
This meant that I could now rewrite my usage like this:
var interviewStateResponse = client.getInterviewState(applicationId);if (interviewStateResponse.stateIs(NOT_BOOKED)) {// do whatever if the interview not yet booked}
Which avoids the "magic constant" literal string and gives me the benefit of static typing internally, while accepting the fact that the inter-service communication cannot be type-checked at compile time. If new interview states are added that do not need to be handled in the same way as 'not yet booked', they can be accepted without modification to my upstream service.
I do not argue this is a perfect solution! You might say that the shared schema between these services points to a problem with the system's architecture. I might agree! Unfortunately, the thing about software architecture is that it's not easy to change once you have it. Nor am I in a position to do anything about the fact that the services are maintained by siloed teams. My target audience here is fellow software engineers working on projects in environments where these decisions were made long ago, and our job is to do the best we can with what we've got. I hope my advice might help some of you!