Key Takeaways
- The modern programming world is split between highly trained professionals who work on low-level code and people who don’t have a deep background but focus on high-level app development that helps meet business requirements.
- Ballerina is an open source programming language, which focuses on improving the productivity of the latter group by providing necessary abstractions, tools, and platforms to create cloud native applications.
- Ballerina treats the network differently by making networking abstractions like client objects, services, resource functions, and listeners a part of the language’s type system. So developers can use the language-provided types to write network programs that just work.
- Ballerina's network and data-friendly type system enables developers to model network communication patterns in an intuitive way that enables maximum productivity.
- Built-in cloud support helps to generate corresponding deployment artifacts by reading defined annotation in source code. These artifacts can be Dockerfiles, Docker images, Kubernetes YAML files, or serverless functions.
- Ballerina’s abstractions and syntax for concurrency and network interaction have been designed so that there is a close correspondence with sequence diagrams.
Earlier this year, Mike Loukides, the VP of content strategy at O’Reilly Media, published a great article on “Rethinking programming.” It explores how the programming world is splitting between highly trained professionals and people who don’t have a deep background but have a lot of experience building things that meet business requirements.
The former group is involved with bundling tools, frameworks, and platforms while the latter group focuses on creating applications for their business by integrating different systems together.
Almost all general-purpose programming languages are designed with abstractions and constructs for highly trained professionals to solve some low-level programming problems.
Ballerina, an open-source programming language, introduces new and fundamental abstractions to help professionals who want a very agile process and use cloud platforms to scale their application with respect to their business requirements.
The Network in the Language
For decades, programming languages have treated networks simply as I/O sources. Because of that, to expose simple APIs, developers have to implement these services by writing an explicit loop that waits for network requests until a signal is obtained. Ballerina treats the network differently by making networking abstractions like client objects, services, resource functions, and listeners a part of the language’s type system so you can use the language-provided types to write network programs that just work.
Using service
type and a listener
object in Ballerina, developers can expose their APIs by simply writing API-led business logic within the resource function
. Depending on the protocol defined in the listener
object, these services can be exposed as HTTP/HTTPS, HTTP2, gRPC, and WebSockets. Ballerina services come with built-in concurrency. Every request to a resource method is handled in a separate strand (Ballerina concurrent unit) and it gives implicit concurrent behavior to a service.
The following code block illustrates a syntax service
type in Ballerina.
import ballerina/http;
service serviceName on new http:Listener(8080) {
resource function newResource(http:Caller caller, http:Request request) {
// API-led logic
}
}
In the request-response paradigm, network communication is done by blocking calls, but blocking a thread to a network call is very expensive. Many language frameworks support async invocations, but developers have to implement async/await by using callback-based code techniques. The following Ballerina code snippet shows a simple HTTP GET operation which is seemingly a blocking operation for the developer. However, internally it does an asynchronous execution using non-blocking I/O, where the current execution thread is released to the operating system to be used by others.
http:Client clientEndpoint = new("http://example.com");
public function main() {
var response = clientEndpoint->get("/get?test=123");
-----
}
Due to unreliable network behavior, remote invocations of APIs are vulnerable to failures. Sometimes an automatic retry will help recover from such failures. In some cases, failover techniques will help to have uninterrupted service delivery or techniques like circuit breakers help to prevent catastrophic cascading failure across multiple programs. Shipping these techniques as part of Ballerina’s client object helps developers to write resilient and robust code with remote network invocations.
The following code snippet shows how to configure a circuit breaker to handle network-related errors in the Ballerina HTTP client object.
http:Client backendClientEP = new("http://example.com", {
circuitBreaker: {
rollingWindow: {
timeWindowInMillis: 10000,
bucketSizeInMillis: 2000,
requestVolumeThreshold: 0
},
failureThreshold: 0.2,
resetTimeInMillis: 10000,
statusCodes: [400, 404, 500]
},
timeoutInMillis: 2000
});
Network services have to work with various user input data. In general, all user input can be dangerous if this isn’t properly checked. Taint analysis is designed to increase security by preventing any variable that can be modified by user input. Ballerina’s built-in taint analyzer helps to identify untrusted (tainted) data by observing how tainted data propagates through the program. If untrusted data is passed to a security-sensitive parameter, a compiler error is generated. Since the taint check happens at the compiler stage, the programmer can then redesign the program to erect a safe wall around the dangerous input.
Network and Data-Friendly Type System
Ballerina supports structural type systems and works with an abstraction of a value called a shape. A shape basically ignores the storage identity of a value. This means it does not consider the name reference of a value when it is compared with other values. This is particularly useful when combining data from multiple, independently-designed systems.
Ballerina also supports Union
types, where sets of values are the union of the value spaces of its component types. For example, you can use a variable of a union type to store a string
or an int
, but there is only one type of value at any given time.
Ballerina has built-in network data type support such as JSON and XML. Ballerina json
type is designed for processing data expression in JSON format. It is a built-in name for a union defined as follows:
type json = () | boolean | int | float | decimal | string | json[] | map<json>
By default, Ballerina has open records. Let’s take a look at a record type to represent the details of a person.
type Gender "male"|"female";
type Address record {|
string city;
string country;
|};
type Person record {
string name;
int birthYear;
Gender gender;
Address address?;
};
Here, the type Person
is an open record type, defined with an inclusive-record-type-descriptor by using the “{
” and “}
” delimiters. The Address
type is a closed record type with an exclusive-record-type-descriptor by using the “{|
” and “|}
” delimiters in the definition. Also in the Person
record address
has the suffix “?
”, making it an optional field where it can be skipped without setting a value.
Let’s create a new type Student
.
type Student record {
string name;
int birthYear;
Gender gender;
Address address?;
string college;
};
The Student
type is a subtype of the Person
type. It has an extra field college
of type string
compared to the Person
type. This is possible because the Person
type is an open type and its shapes can have the string field called college
as well.
public function main() {
Student s1 = { name: "John", birthYear: 1995, gender: "male",
college: "US Berkeley" };
Student s2 = { name: "Kamala", birthYear: 1994, gender: "female",
address: { city: "Milpitas", state: "CA" ,country: "USA"},
college: "Stanford" };
Person p1 = s1;
Person p2 = s2;
io:println("P1's details:" ,p1);
io:println("P2's details:" ,p2);
}
$ ballerina run person.bal
Compiling source
person.bal
Running executables
P1's details:name=John birthYear=1995 gender=male college=US Berkeley
P2's details:name=Kamala birthYear=1994 gender=female address=city=Milpitas state=CA country=USA college=Stanford
Language-integrated query is a feature that allows you to use single syntax against multiple data sources. It will help to break down a complex problem into a series of short, comprehensible queries.
Ballerina query expressions provide a language-integrated query feature using SQL-like syntax. Unlike SQL statements, query expressions help to identify mistakes during design time because of type safety.
import ballerina/io;
type Student record {
string name;
int age;
string school;
};
type Result record {
string name;
string college;
float gpa;
string school;
};
public function main() {
map<float> gpa = {"John": 4.1, "Bill": 3.9, "Sam": 3.3, "Jennifer": 3.1};
Student s1 = {name: "John", age: 18, school: "Milpitas High School"};
Student s2 = {name: "Bill", age: 17, school: "San Jose High School"};
Student s3 = {name: "Sam", age: 18, school: "Clara Coutinho High School"};
Student s4 = {name: "Jennifer", age: 18, school: "Fremont Union High School"};
Student[] student = [];
student.push(s1);
student.push(s2);
student.push(s3);
student.push(s4);
Result[] stanford = from var candidate in student
let float cgpa = (gpa[candidate.name] ?: 0),
string targetCollege = "Stanford"
where cgpa > 3.8
select {
name: candidate.name,
college: targetCollege,
gpa: cgpa,
school: candidate.school
};
io:println(stanford);
}
$ ballerina run query_expression.bal
Compiling source
query_expression.bal
Running executables
name=John college=Stanford GPA=4.1 school=Milpitas High School name=Bill college=Stanford GPA=3.9 school=San Jose High School
The from
clause works similarly to a foreach
statement. It creates an iterator from an iterable value and then binds variables to each value returned by the iterator. The let
clause binds variables. The where
clause provides a way to perform a conditional execution which can refer to variables bound by the from
clause. When the where
condition evaluates to false, the iteration skips the following clauses. The select
clause is evaluated for each iteration and the result of the query expression in this sample is a list
whose members are the result of the select
clause.
Built-in cloud technology integration
Earlier, developers just wrote programs, built it, and ran it. But now they have various ways of running it. It can be in a bare-metal machine or a virtual machine. Or else programs can be packaged as a container and deployed into platforms like Kubernetes and service mesh, or run as serverless programs. However, these deployment options are not part of the programming experience for a developer. The developer has to write code in a certain way to work well in a given execution environment, and removing this from the programming problem isn’t good.
Docker helps to package applications and their dependencies in a binary image that can run in various locations, whether on-premises, in a public cloud, or in a private cloud. To create optimized images, developers have to follow a set of best practices, otherwise, the image that is built will be large in size, less secure, and have many other shortcomings.
The Ballerina compiler is capable of creating optimized Docker images out of the application source code. Adding the @docker:Config {}
to a service
, generates the Dockerfile and a Docker image.
import ballerina/http;
import ballerina/docker;
@docker:Config {
name: "hello",
tag: "v1.0"
}
service Hello on new http:Listener(8080) {
resource function hi(http:Caller caller, http:Request request) returns error? {
check caller->respond("Hello World!");
}
}
$ ballerina build hello.bal
Compiling source
hello.bal
Generating executables
hello.jar
Generating docker artifacts...
@docker - complete 2/2
Run the following command to start a Docker container:
docker run -d -p 8080:8080 hello:v1.0
Kubernetes is an open-source platform for automating deployment, and scaling and management of containerized applications. To deploy and run the program in Kubernetes requires developers to create a set of YAML files. To an average developer, creating these YAML files is not easy. The Ballerina compiler is capable of creating these YAML files while compiling the source code.
import ballerina/http;
import ballerina/kubernetes;
@kubernetes:Service {
serviceType: "NodePort"
}
@kubernetes:Deployment {
name: "hello"
}
service Hello on new http:Listener(8080) {
resource function hi(http:Caller caller, http:Request request) returns error? {
check caller->respond("Hello World!");
}
}
Adding the @kubernetes:Deployment{}
annotation to the Ballerina service will generate the Kubernetes Deployment YAML that is required to deploy our hello application into Kubernetes. Adding the @kubernetes:Service{}
annotation will generate the Kubernetes Service YAML. In this scenario, we have set serviceType
as `NodePort`
to access the hello service via the nodeIP:Port.
$ ballerina build hello.bal
Compiling source
hello.bal
Generating executables
hello.jar
Generating artifacts...
@kubernetes:Service - complete 1/1
@kubernetes:Deployment - complete 1/1
@kubernetes:Docker - complete 2/2
@kubernetes:Helm - complete 1/1
Run the following command to deploy the Kubernetes artifacts:
kubectl apply -f hello/kubernetes
Run the following command to install the application using Helm:
helm install --name helloworld hello/kubernetes/helloworld
In the same way, developers can use Ballerina’s built-in annotations to deploy programs into platforms like OpenShift, Istio, and Knative, or as AWS Lambda functions.
Sequence Diagram
A sequence diagram is the best way to visually describe how services interact. Ballerina’s
abstractions and syntax for concurrency and network interaction have been designed so that there is a close correspondence with sequence diagrams. In Ballerina, a remote method is invoked using a different syntax (->) from a non-remote method. It is depicted as a horizontal arrow from the worker’s lifeline to the client’s object lifeline in a sequence diagram.
In general, developers will write code once and read it many times. In many cases, the code is read by another developer instead of the one who originally wrote it. Having these automated visual representations will help developers to understand program interactions.
The Ballerina IDE plugin (for example, the VSCode plugin) can generate a sequence diagram dynamically from the source code.
More Reading
In this article, we looked at how the unique features of Ballerina enable application developers to write cloud-native applications.
For more extensive language design principles, refer to the Ballerina language specification.
Various examples of using in-built data types, such as JSON/XML, and other network-based functionality can be found in the Ballerina by Example pages.
About the Author
Lakmal Warusawithana is the Senior Director/Developer Relations of WSO2. Lakmal has a long history of working in open source, cloud, and DevOps technologies, and has been Vice President of Apache Stratos PaaS Project. Lakmal is an architect for containerization and deployment orchestration of Ballerina, an open-source programming language for network distributed application. Lakmal has also presented at numerous events, including ApacheCon, CloudOpen, QCon, JaxLondon, Cloud Expo, Cloudstack Collaboration Conference, WSO2Con, KubeCon, ContainerCamp, DeveloperWeek, API Summit, and many tech meetups.