Organizations interested in taking advantage of new platforms such as the public cloud or containers often face challenges in adapting their existing applications to these new environments.
In this post, I examine a method to adapt existing code without changing its intended behavior, refactoring. Let’s look at what refactoring is, and how it can apply to microservice applications.
What is Refactoring?
This section could be a full blog post on its own (and I’m sure it already is in multiple corners of the internet). At its most basic, refactoring is the process of reshaping existing code without changing its functionality. This is often done in order to simplify or streamline source code, with the end goal of making that code more readable or more easily extensible. Successful refactoring may also help discover and fix defects or vulnerabilities in the code.
Refactoring is often an ongoing process made up of a series of small changes. It’s often simpler to maintain the functional behavior of the code when making smaller edits and the consequences of an error are reduced. Modern development tools and Integrated Development Environments (IDEs) may assist with the mechanics of refactoring as well. When code is refactored on a regular basis, these changes may act as a check on “software entropy” introduced by new features and functionality.
There are several common techniques for refactoring source code, which may be organized into a few categories. Usually, these methods are organized into categories such as mapping, abstraction, componentization, moving/renaming code, and clone detection. Each of these divisions consists of multiple methods for refactoring, which may be appropriate for different sections within a piece of software.
At the other end of the scale, an entire program or service may be refactored as a larger effort. This often occurs when a major change in the code is desired, such as a change in framework or programming language. These refactoring projects can be quite complex and make it more difficult to maintain functional consistency. The example explored later in this post is a major one in which a mircroservice is “translated” with the source code moving from Node.js (JavaScript) to Java based on the SpringBoot framework.
Refactoring Microservices
The process for refactoring a microservice is the same as for any other block of source code. That said, since the microservice architecture lends itself to independently testable and loosely coupled pieces, it may be more straightforward to maintain the functional behavior of the code throughout the process.
In the remainder of this post, I’ll breakdown how I went about refactoring a microservice from one language to another. I broke this process into the following steps:
- Functional behavior
- Expected inputs and outputs
- Dependencies
Let’s look at the impact each of these concerns had on the refactoring effort.
Our Example
For this effort, I decided to refactor the Payment microservice from our team’s AMCE Fitness Demo.
Functional Behavior
For our payment service, this section is straightforward and requires little analysis. This microservice provides a REST API endpoint with two routes; a liveness probe on /live
and the payment processing functional accessed by making a POST
call to /pay
. As long as those two requirements were met, functional compatibility was maintained.
Expected Inputs and Outputs
This was the most complex part of the refactor. The payment service is relatively simple, consisting of two API calls. A GET
call can be made to /live
, which simply returns the string live
. This is used by the Order service to verify that payment is up and operating as expected.
The second API available is a POST
request to /pay
, with a JavaScript Object Notation (JSON) body. Within the request body, the microservice is looking for two objects; a card entity (meant to represent a credit card), and a total amount to be paid. Calls to the /pay
API within the ACME Fitness application originate with the order service, which expects a specific response from payment. An example of the required body object is provided below:
{
"card": {
"number": "5678",
"expYear": "2021",
"expMonth": "01",
"ccv": "123"
},
"total": "123"
}
This input is a “nested” JSON object, which two separate objects; the card and total making up the request body.
The output is more simple, with a single JSON object consisting of five parameters. Here’s an example of a successful payment, with a status code of 200:
{
"success": "true",
"status": "200",
"message": "transaction successful",
"amount": "123",
"transactionID": "3f846704-af12-4ea9-a98c-8d7b37e10b54"
}
In Node.js, the function responsible for handling a successful call to the /pay
API looks like this:
app.post('/pay', function (req, res) {
console.log('POST call to /pay');
...
//check card number
card = req.body.card;
d = new Date();
cardNum = card.number;
curYear = d.getFullYear();
curMonth = d.getMonth()+1;
expYear = Number(card.expYear);
expMonth = Number(card.expMonth);
ccv = card.ccv;
total = Number(req.body.total);
...
//process payment
else {
console.log('payment processed successfully');
tID = uuidv4();
return res.status(200).send({
success: 'true',
status: '200',
message: 'transaction successful',
amount: total,
transactionID: tID
});
}
})
I’ve omitted portions of the code which don’t impact the input or output of the /pay
API. In Javascript, this is fairly simple to implement, requiring the Express web application framework for Node.js, as well as a package to generate UUIDs for each transaction.
Replicating this behavior was a bit more complex in Java and the Spring Boot framework. For the sake of brevity, I’ll try not to include unnecessary code, like my Application.java
file, which runs the Spring Boot application. In order to match the functional behavior of the Node.js code above, I needed to create three Java classes; one for the input body, another for the card object within the input body, and one for the output.
OrderInput.java (input):
package com.example.paymentee;
import com.fasterxml.jackson.annotation.*;
public class OrderInput {
private Card card;
private String total;
@JsonProperty("card")
public Card getCard() { return card; }
@JsonProperty("card")
public void setCard(Card value) { this.card = value; }
@JsonProperty("total")
public String getTotal() { return total; }
@JsonProperty("total")
public void setTotal(String value) { this.total = value; }
}
Card.java:
package com.example.paymentee;
import com.fasterxml.jackson.annotation.*;
public class Card {
private String number;
private String expYear;
private String expMonth;
private String ccv;
@JsonProperty("number")
public String getNumber() { return number; }
@JsonProperty("number")
public void setNumber(String value) { this.number = value; }
@JsonProperty("expYear")
public String getExpYear() { return expYear; }
@JsonProperty("expYear")
public void setExpYear(String value) { this.expYear = value; }
@JsonProperty("expMonth")
public String getExpMonth() { return expMonth; }
@JsonProperty("expMonth")
public void setExpMonth(String value) { this.expMonth = value; }
@JsonProperty("ccv")
public String getCcv() { return ccv; }
@JsonProperty("ccv")
public void setCcv(String value) { this.ccv = value; }
}
Payment.java (output):
package com.example.paymentee;
import java.util.UUID;
import lombok.Data;
@Data
public class Payment {
private final UUID transactionID = UUID.randomUUID();
private String success;
private String message;
private long amount;
public Payment(String success, String message, long amount) {
this.setSuccess(success);
this.setMessage(message);
this.setAmount(amount);
}
public UUID getTransactionID() {
return transactionID;
}
public long getAmount() {
return amount;
}
public void setAmount(long amount) {
this.amount = amount;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getSuccess() {
return success;
}
public void setSuccess(String success) {
this.success = success;
}
}
These files specify the objects needed to create the bodies for the API request and response, but they don’t contain the code to handle requests to the two API calls. That code can be found in the PaymentController.java
file:
package com.example.paymentee;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
public class PaymentController {
@RequestMapping(value = "/live", method = RequestMethod.GET)
public String live() {
return "live";
}
@PostMapping("/pay")
public ResponseEntity<Payment> processPayment(@RequestBody OrderInput oin) {
String parsedTotal = oin.getTotal();
String cardNum = oin.getCard().getNumber();
...
return new ResponseEntity<Payment>(tempPayment, HttpStatus.OK);
}
}
I think it’s important to note here that refactoring code will not always reduce the amount of code involved. In this case, it took four files and additional lines of code to accurately replicate the functionality of the original payment service in the transition from JavaScript to Java.
Dependencies
After making sure that the input and output behavior was consistent between the original and refactored code, it was time to be sure the new code had access to any libraries or packages it might need to function properly.
I handled this issue at two levels. Frist, I looked at the packages installed in the original Node.js project. This is easy in Node.js as the package.json
file is stored within the same directory as the code.
{
"name": "payment",
"version": "1.3.2",
"description": "ACME Shop payment service",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"author": "dillson",
"license": "ISC",
"dependencies": {
"body-parser": "^1.18.3",
"express": "^4.16.4",
"jaeger-client": "^3.17.1",
"jsonwebtoken": "^8.5.1",
"opentracing": "^0.14.4",
"request": "^2.88.0",
"uuid": "^3.3.2"
}
}
Ensuring that the required dependencies are part of the refactored code may not be straightforward between languages, but having a reliable inventory simplifies the process. In the refactored Java code, the import
statements represent the dependencies.
Have you had any recent experiences with refactoring code? Did you follow a similar process to mine, or take a different approach? Feel free to reach out to me or the team on twitter to continue this conversation.