Review of Ballerina, A Programming Language For Microservices And APIs

As the API space has grown and transitioned, there have been several major sea changes in what an effective language in the industry looks like. Moving away from classical server-client architecture and into the realm of serverless or other new paradigms can leave old languages feeling inadequate. Developers have to wrangle, mangle, and force language to do what you need it to do, regardless of whether it was designed for that purpose or not.

To solve this, many have taken to developing new languages specifically tailored to a given solution. While this often works quite well for the given application, this often just moves the issue further down the line. Others have tried to solve this by developing near-universal languages that are still natively supported for a specific type of function. This is the kind of language we’re looking at today.

Ballerina is an interesting new programming language, and one with incredible potential. That being said, what is Ballerina, and why is it valuable? What issues is it trying to solve?

What is Ballerina?

Ballerina is an open-source language developed by WSO2. It was designed specifically to replace configuration-based solutions to solve the initial problem of language difficulties. It is a compiled language, focusing largely on microservice development and integration, and most marketing material makes an effort to point out that Ballerina is “cloud-native”.

To better understand Ballerina, we should first look at the justifications given by Ballerina for its development.

Justification and Motivations

Ballerina’s developers view the world as an ever-shrinking system, where content is moving from classical server-based architectures to serverless, microservices, and service meshes. In other words, the world that runs our code has changed fundamentally, and the previous approaches – and thereby the languages developed for these approaches – are not entirely appropriate.

Code has been abstracted from the underlying systems, and this requires a different style. Fundamentally, it means that the way in which we deliver software and code isn’t really within the “middleware” purview anymore. In a Ballerina presentation, in fact, the statement was made that “middleware is dead, because middleware is anywhere”. In essence, there is no middleware when it’s baked into everything we use. App servers aren’t a thing anymore, and each app is called using isolated bits of code that are part of the overall solution.

This shift in thinking requires a new language based around the idea of packaging everything needed into a single language. The idea hinges on the joining of code and purpose, rather than a further abstraction that often is the root cause of the language difficulties previously mentioned.

Design Principles

A core design principle is the notion of leveraging sequence diagrams. Sequence diagrams are often used to model problems, describe complicated systems, and design APIs. They’re typically separate from the code itself, however. The designers of Ballerina wanted to resolve that, with the idea that a sequence diagram can be made of the code and vice versa, as opposed to drawing up a diagram and then doing something wildly divergent for the actual coding sequence.

A major facet of this approach is that both the textual and graphical syntaxes driving the diagram should have exact parity. In other words, it’s not just a picture, a model, or a diagram, but is an actual, functioning method of understanding, defining, and generating content.

Ballerina published a philosophy describing its design principles.

Another core principle is the idea that networks should not be hidden. In many languages, the network abstraction layer is often buried inside a library, and is rarely, if ever, directly utilized. In Ballerina, this relationship is inverted, and the network itself is seen as a major solution to the code problem. Network data types are included in the language to deliver resiliency, and network failure is designed to be accompanied by incredibly verbose and informative error codes, explanations, and valuations to help guide fixes.

Networking data types are very visible, making networking an important aspect of Ballerina.

Of course, many of those network errors don’t occur in the first place with Ballerina. In promotional materials, Ballerina and its compiler are referred to as “very opinionated.” If an error exists in the codebase, more often than not, Ballerina won’t even allow it to be rendered and included. This by its very nature this results in greater security, as there’s a heavy barrier between poor code and the implementation of poor code.

Perhaps the most major principle with Ballerina is the idea that this is not a research project, a testing language, or some sort of futuristic, half-constructed language. Ballerina’s developers saw a great many mainstream concepts that had great value, but were weaker as separate elements and libraries. Accordingly, by combining these abstractions into a singular solution, boosting what works and removing what doesn’t, Ballerina is designed to be a summation of the collection of known successful elements rather than something starkly new.

Unique Elements of Ballerina

There’s a great many features in Ballerina that are unique – or at the very least, are novel for being combined together. Firstly, their support for structural type systems, and especially their focus on providing strong network-friendly typing and union systems, is a rather broad feature set to include in what seems to be a more ultimately limited, opinionated language. This allows Ballerina to be strict with its specific enforcement of typing, while still allowing a wide range of transformative data types.

As part of this, there’s a wide range of support values within Ballerina. These are roughly sorted into three families:

  • Simple Values – These values include things like boolean, int, float, decimal, and string, and are what we would expect for a basic language.
  • Structured Values – This type of data includes tuples, arrays, maps, records, tables, the powerful (but simplified) XML system, and error code reporting. This allows for a more robust managing of internal data types and extends the language to something more than simple.
  • Behavioral Values – This type of value include function, future, object, stream, and typedesc, and further extends both the power and the ultimate extensibility of Ballerina.

Extensibility is certainly a major part of this picture as well. Ballerina supports something called environment binding. In essence, this is a methodology by which the developer can enforce or recommend environment structures and settings based upon what is required for proper functionality. This is a restrictive step to be sure, but being able to determine the exact environment in which your code is used is a big step forward in terms of design continuity and functional quality assurance.

Bolstering this extensibility is the fact that Ballerina has baked in a great amount of middleware into the language itself. By compiling libraries and code snippets into a single solution, we get a powerful and complex system that doesn’t explicitly require external libraries in order to do more complex functions.

Error handling is a big focus for Ballerina as well. All too often languages simply throw an error that is unhelpful, all the while compiling broken code. Ballerina is designed around the idea that errors, both in code and on the network, is a common thing that occurs, and as such, should be handled as a data type and verbosely explained. The compiler will outright refuse to compile code with errors in it (barring exceptional cases where errors might be expected, such as failed external connections), and if an error is generated, the documentation, code, and explanation is complete and helpful.

In terms of actually using Ballerina, the strong error handling platform, the typing system, and the integrated query and streaming query options are excellent. Being able to make SQL-like queries of both static elements and streaming elements is powerful, and opens up a wide world of possibilities.

Perhaps the most unique and value-adding principle here is the idea of an event-driven runtime. Ballerina is designed to be non-blocking. Unlike other languages, in which a queued call has a reserved resource that needs to be released, Ballerina was built from the ground up to not lock resources and to not block additional queries. They’ve even gone so far as to design a pooling system for blocking elements to get around this issue, ensuring that even blocking systems are rendered non-blocking.

In essence, everything in the Ballerina runtime delivers high performance, but without the coding cost to do so. Many other languages (promotional materials point out Node.js as a prime example) can deliver high performance with high complexity, but Ballerina seeks to deliver high performance without the complexity and without code-heavy implementation.

Finally, Ballerina supports what they have termed “always-on parallelism”. Simply put, an executable entity can have any amount of workers assigned to that entity, and those workers are working in a non-blocking, parallel system. Services form a collection of resources where each resource is invoked using an ingress endpoint, and this endpoint is tied to an incredibly parallel system that allows for concurrent work and parallel processing. This in and of itself is a major boost to efficiency and is a prime selling point.

What Does Ballerina Look Like?

With all of this said, what does Ballerina actually look like? Let’s take a look at an example from Ballerina’s documentation. This example will generate a load balancer, which will balance a request load over several target endpoints.

First, we need to create an endpoint that is tied to 8080 for our mock backend services. We can do this using the following code snippet:

import ballerina/http;
import ballerina/log;
listener http:Listener backendEP = new(8080);

Next, we need to define the load balancing client endpoint to call the backend services, as well as define the set of HTTP clients to be balanced.

http:LoadBalanceClient lbBackendEP = new({

    	targets: [
        	{ url: "https://localhost:8080/mock1" },
        	{ url: "https://localhost:8080/mock2" },
        	{ url: "https://localhost:8080/mock3" }
    	],
    	timeoutMillis: 5000
});

In this case, we have three services that are being load balanced, with a specific millisecond timeout value of 5000ms. We now need to create an HTTP service that is bound to the endpoint, and create a REST resource within the API

@http:ServiceConfig {
	basePath: "/lb"
}
service loadBalancerDemoService on new http:Listener (9090) {

	@http:ResourceConfig {
    	path: "/"
	}

Next, we need to include a caller endpoint reference, as well as the request data object:

	@http:ResourceConfig {
    	path: "/"
	}

	resource function roundRobin(http:Caller caller, http:Request req) {
    	json requestPayload = { "name": "Ballerina" };
    	var response = lbBackendEP->post("/", requestPayload);

This section is the primary error-handling logic router – if a valid response is returned, the normal process will run, but if a non-expected response is generated, this logic is triggered:

    	if (response is http:Response) {
        	var responseToCaller = caller->respond(response);
        	if (responseToCaller is error) {
            	log:printError("Error sending response",
                            	err = responseToCaller);
        	}
    	} else {
        	http:Response outResponse = new;
        	outResponse.statusCode = 500;
        	outResponse.setPayload(response.detail().message);
        	var responseToCaller = caller->respond(outResponse);
        	if (responseToCaller is error) {
            	log:printError("Error sending response", err = responseToCaller);
        	}
    	}
	}
}

Finally, we need to define the backend services that will be called by the load balancer:

@http:ServiceConfig {
	basePath: "/mock1"
}
service mock1 on backendEP {
	@http:ResourceConfig {
    	path: "/"
	}
	resource function mock1Resource(http:Caller caller, http:Request req) {
    	var responseToCaller = caller->respond("Mock1 resource was invoked.");
    	if (responseToCaller is error) {
        	log:printError("Error sending response from mock service", err = responseToCaller);
    	}
	}
}

@http:ServiceConfig {
	basePath: "/mock2"
}
service mock2 on backendEP {
	@http:ResourceConfig {
    	path: "/"
	}
	resource function mock2Resource(http:Caller caller, http:Request req) {
    	var responseToCaller = caller->respond("Mock2 resource was invoked.");
    	if (responseToCaller is error) {
        	log:printError("Error sending response from mock service", err = responseToCaller);
    	}
	}
}
@http:ServiceConfig {
	basePath: "/mock3"
}
service mock3 on backendEP {
	@http:ResourceConfig {
    	path: "/"
	}
	resource function mock3Resource(http:Caller caller, http:Request req) {
    	var responseToCaller = caller->respond("Mock3 resource was invoked.");
    	if (responseToCaller is error) {
        	log:printError("Error sending response from mock service", err = responseToCaller);
    	}
	}
}

Now we have a fully functional, efficient load balancing solution. This, in conjunction with the always-on parallelism solution, will deliver a high-throughput, well-balanced response system.

Conclusion

Ballerina solves a very specific problem, and it does it quite well. That being said, it is a rather heavy lift to ask a developer to shift to a brand new language when many of the libraries and extensible solutions enable the same functions. Assuming you’re starting from scratch, however, Ballerina can be a powerful tool to deliver a more efficient, effective language-driven solution.

What do you think about Ballerina? Do you think it delivers on its promises? Or do you think it’s overhyped? Let us know in the comments section below!