Refactoring Towards Expressive REST APIs: Let Your Code Be Your Guide

March 30, 2016 David Julia

 

sfeatured-pivotallabs-slateAn expressive REST API design can create a delightful experience for consumers, while APIs that are hard to understand can cause endless frustration. We need only look to companies like Github and Twilio for good examples of how a great API can be a true competitive advantage. Github has an entire ecosystem of connected apps that strengthen their offering. Twilio’s API is so easy to use that once I started to use it, the experience is so delightful that I wanted to leverage more features of their platform. There’s tremendous value to be had in building expressive, easy to use APIs. Yet, deficiencies in API expressivity are not always obvious, and once we’ve uncovered them they can be hard to fix.

I recently encountered an example of a RESTful JSON API endpoint that deeply troubled me. On the surface, it seemed alright—the endpoints were generally modeled as resources and made sense in the domain. But, looking at the code for this one endpoint, it just made me feel like things were a bit off. I always had a little bit of trouble figuring out exactly how to call the dang endpoint.

The code had a bunch of branching conditional logic. In fact, it took me a few times of looking at it, over the course of a week or two, before I realized what was really going on. One of the main advantages of RESTful APIs is discoverability—clients can know what resources are available to them based on hypermedia links returned to discrete resources. This implies that resource design should both reveal and enable the intended usage of the API. This discoverability greatly simplifies and improves the robustness of clients by presenting them with the actions available to them from any given state. This is why you often hear the term “self-documenting” thrown around, especially with respect to REST.

Good REST APIs (and good code in general) are largely self-documenting. This endpoint, while superficially RESTful, was not intention revealing, and it was far from self documenting. What follows is an account of how I went about diagnosing, refactoring, and eliminating the high level API resource modeling issue in the large by following and fixing the code smells in the small.

Overview Of The Baseline Architecture And Code

For discussion’s sake, I’ve reproduced a similar API/class structure below. And, I have chosen a different domain with very similar characteristics. Let’s say we have a mobile banking application that uses this API to make deposits, withdrawals, transfers, and even to close accounts. In this simplified example, the API is backed by a legacy banking service that is difficult to interact with but spits out some nice JSON. Of course, in the real world, this gateway would do things like call out to an authentication service, implement some additional audit logging, and all sorts of fancy stuff, but let’s ignore all that for the sake of discussion.

Here’s the Java/Spring code in its original state, give it a once over. Note, the constructors, getters, and setters are omitted for brevity.

@RestController
class AccountActionsController {
   private AccountActionsService service;

   @Autowired
   public AccountActionsController(AccountActionsService service) {
       this.service = service;
   }

   @RequestMapping(value = "/accounts/{accountNumber}/", method = RequestMethod.POST)
   public AccountActionsApiResponse createAccountAction(
     @RequestBody AccountAction action) {
       return new AccountActionsApiResponse(service.performAccountAction(action));
   }

}

class AccountActionsService {
   private LegacyAccountsSystemsClient client;
   private AccountActionRequestFactory legacySystemRequestFactory;

   public AccountActionsService(LegacyAccountsSystemsClient client, AccountActionRequestFactory legacySystemRequestFactory) {
       this.client = client;
       this.legacySystemRequestFactory = legacySystemRequestFactory;
   }

   public AccountActionResult performAccountAction(AccountAction action) {
       return client.performRequest(legacySystemRequestFactory.make(action));
   }
}

class AccountActionRequestFactory {
   public AccountActionRequest make(AccountAction action) {
       switch (action.getType()) {
           case WITHDRAWAL:
               return new AccountActionRequest("WDR", action.getAccountNumber(), action.getAmount());
           case DEPOSIT:
               return new AccountActionRequest("DPT", action.getAccountNumber(), action.getAmount());
           case TRANSFER:
               return new AccountActionRequest("XFR", action.getAccountNumber(), action.getAmount(), action.getDestinationAccountNumber());
           case CLOSE:
               return new AccountActionRequest("CLS", action.getAccountNumber());
       }
       throw new RuntimeException("Unsupported Operation");
   }
}


class AccountAction {
   private final Integer accountNumber;
   private final Integer amount;
   private final Integer destinationAccountNumber;
   private ActionType type;
   …
}


//We can't change this DTO, it's the format that the Legacy Service expects of our requests and
//was generated from a wsdl.
class AccountActionRequest {
   @JacksonXmlProperty(localName = "withdrawal_amt")
   private Integer withdrawalAmount;

   @JacksonXmlProperty(localName = "cmd_cd")
   private String commandCode;
   private final Integer accountNumber;

   @JacksonXmlProperty(localName = "dst_act_nbr")
   private Integer destinationAccountNumber;

   @JacksonXmlProperty(localName = "trxn_amt")
   private Integer amount;

   public AccountActionRequest(String commandCode, Integer accountNumber, Integer amount) {
       this.commandCode = commandCode;
       this.accountNumber = accountNumber;
       this.amount = amount;
   }

   public AccountActionRequest(String commandCode, Integer accountNumber, Integer amount, Integer destinationAccountNumber) {

       this.commandCode = commandCode;
       this.accountNumber = accountNumber;
       this.amount = amount;
       this.destinationAccountNumber = destinationAccountNumber;
   }

   public AccountActionRequest(String commandCode, Integer accountNumber) {
       this.commandCode = commandCode;
       this.accountNumber = accountNumber;
   }
}

The Problems With The Current Implementation

The first and most glaring thing staring us in the face is a big old switch case. We’ve been good Object Oriented Programmers and we’ve hidden it inside of a factory, but there’s still something unsettling about it. We’ll come back to that in a second.

The next major problem is the AccountAction, an object we have defined ourselves, is used for all sorts of operations. What is AccountAction? When is it valid? What combination of fields being set or null make it valid for various types of actions? In the real life code that inspired this, there were more than ten action types and about fifteen different fields. Some of these were always required and other combinations were meant to be null for certain situations and task types.

This approach doesn’t convey intent. It doesn’t scream “I’m meant to be used this way.” I have to construct it, and then know what fields are meant to be set for various situations. I have to know. That’s a problem. I shouldn’t have to know a darn thing. Clean code conveys its intent and fits the problem like a glove. This code fits the problem like a pair of oversized sweat pants.

If the AccountAction object itself doesn’t convey intent, what does that say for our API as a whole? What exactly are the operations that I can perform using the API? I can’t inspect the available endpoints and understand what the API should do. Good luck coming up with semantic URLs for hypermedia links to represent possible application state transitions.

Redesigning The API

All this grandstanding and criticism is fun, but let’s talk alternatives. Let’s imagine what a nice RESTful API with good hypermedia links would look like. Instead of using that bothersome switch case on action type, what if we represented each of those operations as separate endpoints? So we’d have the following endpoints:

  • POST /accounts/{accountNumber}/withdrawal
  • POST /accounts/{accountNumber}/deposit
  • POST /accounts/{accountNumber}/transfer
  • POST /accounts/{accountNumber}/close

All endpoints would link to the applicable operations. Namely withdrawal, deposit, and transfer would link to all the other resources, while close would link to no other resources. This API codifies the available operations in both the URLs themselves and the hypermedia links that are presented in response. This seems pretty intention revealing to me.

Now, let’s see what kind of design pressure this new API structure imposes on our server side code. First, our whole factory can go away. We no longer need to switch on action type to know the exact request we need to make to our legacy service.

Our controller will now expose intention revealing, resourceful endpoints:

@RestController
class AccountActionsController {
   public static final String WITHDRAWAL_PATH = "/accounts/{accountNumber}/withdrawal";
   public static final String DEPOSIT_PATH = "/accounts/{accountNumber}/deposit";
   public static final String TRANSFER_PATH = "/accounts/{accountNumber}/transfer";
   public static final String CLOSE_ACCOUNT_PATH = "/accounts/{accountNumber}/close";
   private AccountActionsService service;

   public AccountActionsController(AccountActionsService service) {
       this.service = service;
   }

   @RequestMapping(value = WITHDRAWAL_PATH, method = RequestMethod.POST)
   public AccountActionsApiResponse createWithdrawalRequest(WithdrawalRequest withdrawal) {
       return new AccountActionsApiResponse(service.performWithdrawal(withdrawal), WITHDRAWAL_PATH, DEPOSIT_PATH, TRANSFER_PATH, CLOSE_ACCOUNT_PATH);
   }

   @RequestMapping(value = DEPOSIT_PATH, method = RequestMethod.POST)
   public AccountActionsApiResponse createDepositRequest(DepositRequest deposit) {
       return new AccountActionsApiResponse(service.performDeposit(deposit), WITHDRAWAL_PATH, DEPOSIT_PATH, TRANSFER_PATH, CLOSE_ACCOUNT_PATH);
   }

   @RequestMapping(value = TRANSFER_PATH, method = RequestMethod.POST)
   public AccountActionsApiResponse createTransferRequest(TransferRequest transfer) {
       return new AccountActionsApiResponse(service.performTransfer(transfer), WITHDRAWAL_PATH, DEPOSIT_PATH, TRANSFER_PATH, CLOSE_ACCOUNT_PATH);
   }

   @RequestMapping(value = CLOSE_ACCOUNT_PATH, method = RequestMethod.POST)
   public AccountActionsApiResponse createCloseAccountRequest(AccountCloseRequest close) {
       return new AccountActionsApiResponse(service.closeAccount(close));
   }

}

We’re now better positioned to respond differently to each type of request. Should the need arise in the future, we only need to change the response for each endpoint to a more specific type. This is a slightly naive approach to hypermedia—we always return the operations that could conceivably be available. There is a richer approach to hypermedia—only return the withdrawal link if you had a positive balance, and perhaps you’d only return the close account link if you had a nonnegative balance. In fact, with our chosen modeling, we’re well positioned to move towards a richer API in general. We modeled all of the individual endpoints as creations (POSTs) because we’re creating a request for the bank to take an action. We can’t take action directly on the account- that’s the bank’s responsibility once processing has taken place. Thus, the account action is the request for the bank to take action action, and that is the resource. In REST, we could express this as follows:

POST /accounts/3123abc/close could return something like the following:

Location: http://api.bank.example.com/accounts/3123abc/close/123
Status: 201 CREATED
{ "status": "closing", "estimatedDaysUntilFundsDispersed": 2}
 

A day later, the corresponding GET to http://api.bank.example.com/accounts/3123abc/close/123 would return something like:

Status: 200 OK
{ "status": "closing", "estimatedDaysUntilFundsDispersed": 1}

Perhaps you could even cancel a close account request with a DELETE to http://api.bank.example.com/accounts/3123abc/close/123. By modeling these account actions as resources, we allow for easy querying about the status and canceling of the request, among other things.

Potential enhancements aside, with our newly refactored code, the service exposes these operations as first class citizens of the domain:


class AccountActionsService {
   private LegacyAccountsSystemsClient client;

   public AccountActionsService(LegacyAccountsSystemsClient client) {
       this.client = client;
   }

   public AccountActionResult performWithdrawal(WithdrawalRequest withdrawal) {
       return client.performRequest(new LegacyAccountActionRequest("WDR", withdrawal.getAccountNumber(), withdrawal.getAmount()));
   }

   public AccountActionResult performDeposit(DepositRequest deposit) {
       return client.performRequest(new LegacyAccountActionRequest("DPT", deposit.getAccountNumber(), deposit.getAmount()));
   }

   public AccountActionResult performTransfer(TransferRequest transfer) {
       return client.performRequest(new LegacyAccountActionRequest("XFR", transfer.getSourceAccountNumber(), transfer.getAmount(), transfer.getDestinationAccountNumber()));
   }

   public AccountActionResult closeAccount(AccountCloseRequest close) {
       return client.performRequest(new LegacyAccountActionRequest("CLS", close.getAccountNumber()));
   }

}

There is another benefit here—we have a great place to do operation specific error handling in each of the methods of the Service class. In real application code, I imagine that there are a lot of error cases we’d want to handle for different kind of failures. In the original code, with only one performAccountAction method in the service, error handling code would likely have been littered with checks that only apply to specific kinds of transactions.

The Outcomes Of The API Redesign

We are left with a nice set of value objects representing the Domain concepts. These are all single purpose and reveal their intent. Sure they aren’t perfect- for example, they still suffer from some primitive obsession, but this is much better than before. All of the fields in each Domain object are always required. Thus, I know at a glance that a TransferRequest needs both a source and destination account, along with the amount.To make a withdrawal, I only need an accountNumber and amount.

This is a huge improvement over the previous AccountAction, where we need to know the required fields for each operation. This refactoring takes implicit knowledge and makes it explicit. The API is now self documenting—we don’t have to keep that knowledge in our heads or in some external documentation which may get out of sync.


class WithdrawalRequest {
   private final Integer accountNumber;
   private final Integer amount;
   ...
}

class DepositRequest {
   private final Integer amount;
   private final Integer accountNumber;
   ...
}

class AccountCloseRequest {
   private Integer accountNumber;
   ...
}

class TransferRequest {
   private final Integer amount;
   private final Integer sourceAccountNumber;
   private final Integer destinationAccountNumber;
   ...
}

The main win here was in recognizing the opportunity to replace a switch case with individual resourceful endpoints. We built a self documenting api, which is our published interface.

Builders, factories, and their kind are wonderful tools, but they should not be used to mask the underlying issue. The factory made the original code bearable, but it was just a bandaid. By solving the underlying design problem, we have accomplished two things. First, we got of the original code smell and eliminated the need for the factory, and we also made our published interface, our API better.

So what’s the takeaway? Firstly, code smells in lower level components may point you to deficiencies in your high level design. Secondly, APIs should communicate intent. Thirdly, modeling operations as RESTful resources really pushes us towards self documenting, intention revealing APIs where possible actions are presented as hypermedia links. And finally, look for opportunities to take implicit knowledge and express it explicitly in your designs.

 

Special thanks to Mike Gehard for reviewing this post and providing incredibly constructive criticism that drove a revision on 04/12/2016. He pointed out a misuse of the term polymorphism as well as an opportunity to better explain the resource modeling.
Additionally, here is one note of clarification on this post in general stemming from Mike’s feedback on my use of the term refactoring in this blog post:
Per the definition on refactoring.com, Refactoring “is a disciplined technique for restructuring an existing body of code, altering its internal structure without changing its external behavior.”

Without any context on the calling code, the above code transformations are not a true refactoring. In the example that inspired this, it was an API for an Android app, and we could make atomic refactorings across client and server. So in that sense, such an API redesign would be a refactoring in the context of the overall system if we consider the API internal and changed our clients to use the new API without changing visible behavior. Without that context, it would be a stretch for me to call the API redesign a true refactoring.

Recommended Reading:

 

About the Author

Biography

More Content by David Julia
Previous
Cloud Foundry Now Supports VMware Photon Platform
Cloud Foundry Now Supports VMware Photon Platform

Today, VMware and Pivotal share an important milestone in our promise to deliver a next generation, turnkey...

Next
Scoring-as-a-Service To Operationalize Algorithms For Real-time
Scoring-as-a-Service To Operationalize Algorithms For Real-time

In this post, two of Pivotal’s top data scientists share the ultimate formula for data science—operationali...

×

Subscribe to our Newsletter

Thank you!
Error - something went wrong!