Microservices is an architectural style used for creating applications made of individual fine-grained services, which encourages functional decoupling, re-usability, and scalability. This is becoming increasingly popular compared to the monolithic applications we generally build. Monoliths generally lack the flexibility given by microservices in separating out functional components to have their own independent development teams, lifecycles, and deployment.
Java still remains the most popular language developers use in developing enterprise applications. And with no surprise, there are plenty of Java frameworks that support microservices development as well. Among these, Spring Boot is arguably the most popular option out there. Amidst these options, Ballerina takes a different approach in providing a programming language purpose-built for optimizing microservices development (full disclosure - I contribute to the Ballerina project).
In this post, I will start with a Spring Boot application implementation and show the same functionality implemented using Ballerina. The sample use case I’ll be implementing is based on a student registry scenario. Here, an in-memory registry is kept to insert, lookup, update, and delete student records. The interface for this registry is through an HTTP API. In order to cover the most often used functionality in writing similar services, I’ve added the following requirements to the use case.
Let’s first start off with the Spring Boot implementation.
We will be using Gradle to set up and build our Java project. The following build.gradle file contains the basic configuration to set up a typical Spring Boot service application.
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:2.2.1.RELEASE")
}
}
apply plugin: 'java'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
bootJar {
baseName = 'student-registry-service'
version = '0.1.0'
}
repositories {
mavenCentral()
}
sourceCompatibility = 1.8
targetCompatibility = 1.8
dependencies {
compile("org.springframework.boot:spring-boot-starter-web")
testCompile('org.springframework.boot:spring-boot-starter-test')
}
Create a directory named “student_registry” and drop in the above file. Also, create the directory structure “
src/main/java/org/demo
” inside this directory. In Unix-like systems, you can use the following command.mkdir -p src/main/java/org/demo
Inside the “org.demo” Java package, we will be having three classes. These are introduced below.
The following "Application.java" file is the entry point to the Java application, which contains the main method. This will bootstrap the Spring application by locating the service implementations in the classpath and initialize the required components.
package org.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
The following "Student.java" file represents the model class to represent the resource information used in the service.
package org.demo;
public class Student {
private String id;
private String name;
private Major major = Major.CS;
public String getId() {
return id;
}
public String getName() {
return name;
}
public Major getMajor() {
return major;
}
public void setId(String id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
public void setMajor(Major major) {
this.major = major;
}
}
enum Major {
CS,
Physics,
Chemistry
}
The following "StudentRegistryController.java" file contains the class representing the resource controller which is used as the target for where the messages are dispatched to. It will contain annotations that map request patterns to methods.
package org.demo;
import java.util.HashMap;
import java.util.Map;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
@RestController
public class StudentRegistryController {
private Map<String, Student> registry = new HashMap<>();
@GetMapping("/registry/{id}")
public Student lookupStudent(@PathVariable("id") String id) {
Student student = this.registry.get(id);
if (student == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Student with the given id does not exist");
}
return student;
}
@PostMapping("/registry/")
public void addStudent(@RequestBody Student student) {
if (this.registry.containsKey(student.getId())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Student with the given id already exists");
}
this.registry.put(student.getId(), student);
}
@PutMapping("/registry/")
public void updateStudent(@RequestBody Student student) {
if (!this.registry.containsKey(student.getId())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Student with the given id does not exist");
}
this.registry.put(student.getId(), student);
}
@DeleteMapping("/registry/{id}")
public void deleteStudent(@PathVariable("id") String id) {
this.registry.remove(id);
}
}
With the above source files available in our Java package, we can build the project by executing “gradle build” in the base directory. The jar file that is created contains all the dependencies and the service implementations added. We can now simply run the generated executable jar file with the following command.
java -jar build/libs/student-registry-service-0.1.0.jar
The execution starts up its own embedded web server and starts serving requests to the service.
A sample session with the service is shown below.
$ curl -H "Content-Type: application/json" -X POST -d '{"id":"W01", "name":"Jack", "major":"CS"}' http://localhost:8080/registry/
$ curl http://localhost:8080/registry/W01
{"id":"W01","name":"Jack","major":"CS"}
$ curl -H "Content-Type: application/json" -X PUT -d '{"id":"W01", "name":"Jack", "major":"Physics"}' http://localhost:8080/registry/
$ curl http://localhost:8080/registry/W01
{"id":"W01","name":"Jack","major":"Physics"}
$ curl -X DELETE http://localhost:8080/registry/W01
$ curl http://localhost:8080/registry/W01
{"timestamp":"2019-11-27T05:27:40.185+0000","status":404,"error":"Not Found","message":"Student with the given id does not exist","path":"/registry/1"}
$ curl -H "Content-Type: application/json" -X POST -d '{"id":"W02", "name":"Saman"}' http://localhost:8080/registry/
$ curl http://localhost:8080/registry/W02
{"id":"W02","name":"Saman","major":"CS"}
In order to view the return HTTP status code, the curl command can be given the “-v” switch to enable verbose mode.
In the next section, let’s see how we can implement the same with a Ballerina service.
Ballerina has two entry points to its applications. It can either be a main function or a service. For our use-case, a service is directly applicable. Let’s look at the full Ballerina implementation shown in the following "student_registry.bal" file and how to build and run the service.
import ballerina/http;
enum Major {
CS, Physics, Chemistry
}
type Student record {|
string id;
string name;
Major major = CS;
|};
map<Student & readonly> students = {};
service /registry on new http:Listener(8080) {
resource function get [string id]() returns Student|http:NotFound {
Student? student;
lock {
student = students[id];
}
if student is () {
return {body: "Student with the given id does not exist"};
} else {
return student;
}
}
resource function post .(@http:Payload Student student) returns http:Ok|http:BadRequest {
lock {
if students.hasKey(student.id) {
return <http:BadRequest> {body: "Student with the given id already exists"};
}
students[student.id] = student.cloneReadOnly();
}
return <http:Ok> {};
}
resource function put .(@http:Payload Student student) returns http:BadRequest|http:Ok {
lock {
if !students.hasKey(student.id) {
return <http:BadRequest> {body: "Student with the given id does not exist"};
}
students[student.id] = student.cloneReadOnly();
}
return <http:Ok> {};
}
resource function delete [string id]() returns http:Ok {
lock {
_ = students.removeIfHasKey(id);
}
return {};
}
}
The above code can be built and run using the following commands.
bal build student_registry.bal
bal run student_registry.jar
The Ballerina service implementation is similar in functionality to the earlier defined Spring Boot application. Thus, we can use the same HTTP requests in testing the service.
Now let’s analyze the code to see how each aspect of the Spring Boot service is fulfilled by the Ballerina service.
The resource model implementation in the Spring Boot application, done using the “Student” Java bean class, is done using the “Student” record type in Ballerina. Since this is a structure that simply needs data and no behavior, the record type is the exact candidate for this. Also, an enumeration “Major” is defined using constants to represent the possible major values for the student.
One of these values is also given as a default value in the “major” field in the “Student” record type. The default values, along with other record type level concepts such as optional fields and values, are directly handled by the data binding operations of Ballerina services.
The Spring Boot application and the REST controller is represented by the Ballerina service construct. A Ballerina service represents a network service, which is bound to a specific type of network listener. Here, we have used an HTTP listener, but this can also be other types of network listeners, such as gRPC, WebSocket and so on.
Individual resource functions represent the service mappings of incoming individual requests. These are similar to the Spring Boot REST Controller methods. Service and resource level annotations are used to provide additional metadata on the services, such as base paths, resource paths, and supported HTTP methods.
Also, in Ballerina, we have access to the incoming request caller, in order to send explicit responses back to the client — which is done using caller->respond(). This has the added advantage of the user having full control of the return communication back to the client. For example, if there is an error in the response communication, this is directly seen and accessible in the Ballerina code. Thus we can provide additional logic to handle this scenario. In the Spring Boot code, we return a Java object or we throw an exception to signal the return communication to the client. And thereafter, we do not directly have access to the return communication back to the client.
In Ballerina, an additional aspect that is covered is the auto-generation of the sequence diagrams for the code. This is possible since the Ballerina language itself is designed ground-up to be based on a sequence diagramming concept. Here, all our clients, listeners, and remote endpoints are visualized as actors in the diagram, and the messages passing between them are shown using remote function calls denoted using the “->” operator.
The above explanation shows a scenario on how to convert a typical Spring Boot HTTP service application to a Ballerina service. Here are a few resources that were used in the article and will be useful to you to learn Ballerina further: