Ballerina: The Programming Language for the Cloud and Your Integration Needs

13.03.2024Stefan Welsch
Cloud Cloud Computing Developer Experience Microservices Serverless open-source programming

Banner

Finally, a New Programming Language Again

Today, I’m dedicating my time to Ballerina, an open-source, cloud-native programming language optimized for integration. The website even states:

“Ballerina is the ONLY programming language designed for integration.”

It is developed and supported by WSO2. According to the developers, Ballerina should make it possible to easily develop microservices, API endpoints and integrations, as well as any other application for the cloud. I want to take a closer look at this today and find out what the new programming language is really capable of.

But first, a few details about Ballerina. The first release of Ballerina was on May 1st, 2019. The semantics of Ballerina are not defined by the implementation, but by specifications. Currently, there is only one implementation of this specification (jBallerina) which, as the name suggests, compiles the source code into Java bytecode. However, there are supposed to be other implementations in the future. A look at the Github Page of Ballerina is also worthwhile. Here you have a pretty good overview of which modules and extensions exist around the language and also how actively it is currently being developed.

So, let’s get started. First, you need to install the Ballerina distribution for your respective system. For me as a Mac user with Homebrew, it’s quite simple:

1
2
3
4
5
> brew install bal
> bal version
Ballerina 2201.8.5 (Swan Lake Update 8)
Language specification 2023R1
Update Tool 1.4.2

After that, I can already start with my first experiment. Visual Studio Code is a good choice for development. There is a nice Extension here, which, in addition to the usual syntax highlighting and code completion, also provides a corresponding Dev Container if we want it. There is also a UI with which we can display the code as a kind of sequence diagram (service diagram) and also edit the code via the diagram. There is also an HTTP and a GraphQL API designer.

But now let’s turn to the first example and start with a simple “Hello World” example. We can create this with the following command:

1
bal new hello-world 

Let’s look at the structure of the project. To do this, we open the project in Visual Studio Code.

image-20240229075738599

There is the file Ballerina.toml which contains various metadata. In addition, a .gitignore and a .devcontainer.json file were created, with which we can start the DevContainer, as mentioned above. For us, however, only the file main.bal is important for now, in which we can write our code.

From the syntax, we see that the language looks like a mixture of Java and Go. As we can see, there are now 3 “buttons” above the main() method that we can click. So let’s first run the application by clicking on “Run”. image-20240229082231618

Now we are less interested in what happens in the output, but rather what exactly happens when we click on Run. In the target folder, we see, as mentioned above, that the source has been compiled into a Java application. In the output, we naturally see a “Hello, World!”.

Ok, let’s go one step further and try to call an HTTP API. For this, Ballerina offers us the http module.

So we change our code in the main.bal as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import ballerina/http;

service / on new http:Listener(9090) {

    resource function get breeds() returns json|error? {
        http:Client jcClient = check new ("https://api.thedogapi.com/");
        map<string> headers = {
            "x-api-key": "***"
        };

        json response = check jcClient->/v1/breeds('limit = 3, headers = headers);

        return response;
    }
}

With the code, we want to search for dog breeds at the Dog API. You can create a free API key there if you want to play with it.

In line 3, we create an HTTP listener that is bound to port 9090. So when our application is started, we can call it on port 9090. This is essentially our server. On line 5, we then register a GET endpoint that listens on the path breeds. As a return value, we expect either a json or an error if there is a problem calling the Dog API. In line 6, we create an HTTP client that can communicate with the Dog API endpoint.

[!NOTE]

The check keyword saves us the error handling in Ballerina. Instead, we simply throw the error to the calling method or block. Alternatively, we could also do the following to handle the error immediately.

1
2
3
4
http:Client|http:ClientError jcClient = new ("https://api.thedogapi.com/");
if jcClient is error {
  // handle error here
}

Lines 7-9 simply create the header that we need to send to the Dog API endpoint for authentication. On line 11, we now execute the request against the Dog API. With 'limit we specify a query parameter that tells the service how many results we expect back, and additionally we also include the api-key header. As a response, we expect a json, which we return to the calling client.

We can now start our program with the following command:

1
2
3
4
5
> bal run
Compiling source
        swelsch/hello_world:0.1.0

Running executable

Now we open a second terminal and test our newly created endpoint.

1
2
❯ curl localhost:9090/breeds                                         4s
[{"weight":{"imperial":"6 - 13", "metric":"3 - 6"}, "height":{"imperial":"9 - 11.5", "metric":"23 - 29"}, "id":1, "name":"Affenpinscher", "bred_for":"Small rodent hunting, lapdog", "breed_group":"Toy", "life_span":"10 - 12 years", "temperament":"Stubborn, Curious, Playful, Adventurous, Active, Fun-loving", "origin":"Germany, France", "reference_image_id":"BJa4kxc4X", "image":{"id":"BJa4kxc4X", "width":1600, "height":1199, "url":"https://cdn2.thedogapi.com/images/BJa4kxc4X.jpg"}}, {"weight":{"imperial":"50 - 60", "metric":"23 - 27"}, "height":{"imperial":"25 - 27", "metric":"64 - 69"}, "id":2, "name":"Afghan Hound", "country_code":"AG", "bred_for":"Coursing and hunting", "breed_group":"Hound", "life_span":"10 - 13 years", "temperament":"Aloof, Clownish, Dignified, Independent, Happy", "origin":"Afghanistan, Iran, Pakistan", "reference_image_id":"hMyT4CDXR", "image":{"id":"hMyT4CDXR", "width":606, "height":380, "url":"https://cdn2.thedogapi.com/images/hMyT4CDXR.jpg"}}, {"weight":{"imperial":"44 - 66", "metric":"20 - 30"}, "height":{"imperial":"30", "metric":"76"}, "id":3, "name":"African Hunting Dog", "bred_for":"A wild pack animal", "life_span":"11 years", "temperament":"Wild, Hardworking, Dutiful", "origin":"", "reference_image_id":"rkiByec47", "image":{"id":"rkiByec47", "width":500, "height":335, "url":"https://cdn2.thedogapi.com/images/rkiByec47.jpg"}}]

As we can see, we get 3 results back from our service as expected.

Now we want to store the names and life expectancy of the animals in a database. We use a serverless AWS DynamoDB database internally, and fortunately, Ballerina already provides us with a corresponding extension. But before we store the values in the DB, we first want to store them in a specific type. Ballerina also offers us a type very similar to golang for this purpose. So we extend our main.bal file with the following content:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import ballerina/http;
import ballerina/os;

type Breed readonly & record {
    int id;
    string name;
    string life_span;
};


service / on new http:Listener(9090) {

    resource function get breeds() returns Breed[]|error? {
        http:Client jcClient = check new ("https://api.thedogapi.com/");

        Breed[] breeds = check jcClient->/v1/breeds('limit = 3, has_breeds=1, headers = {
            "x-api-key": os:getEnv("THE_DOG_API_KEY")
        });

        return breeds;
    }

So we no longer store the response from the web service in a generic response, but in a so-called Record. This is a so-called open record. We’ll see what that is in a moment.

If we now start our program and again execute a curl on the endpoint breeds, we surprisingly get the same result as before:

1
2
> curl localhost:9090/breeds_record
[{"id":1, "name":"Affenpinscher", "life_span":"10 - 12 years", "image":{"id":"BJa4kxc4X", "width":1600, "height":1199, "url":"https://cdn2.thedogapi.com/images/BJa4kxc4X.jpg"}, "breed_group":"Toy", "temperament":"Stubborn, Curious, Playful, Adventurous, Active, Fun-loving", "origin":"Germany, France", "weight":{"imperial":"6 - 13", "metric":"3 - 6"}, "bred_for":"Small rodent hunting, lapdog", "reference_image_id":"BJa4kxc4X", "height":{"imperial":"9 - 11.5", "metric":"23 - 29"}}, {"id":2, "name":"Afghan Hound", "life_span":"10 - 13 years", "country_code":"AG", "image":{"id":"hMyT4CDXR", "width":606, "height":380, "url":"https://cdn2.thedogapi.com/images/hMyT4CDXR.jpg"}, "breed_group":"Hound", "temperament":"Aloof, Clownish, Dignified, Independent, Happy", "origin":"Afghanistan, Iran, Pakistan", "weight":{"imperial":"50 - 60", "metric":"23 - 27"}, "bred_for":"Coursing and hunting", "reference_image_id":"hMyT4CDXR", "height":{"imperial":"25 - 27", "metric":"64 - 69"}}, {"id":3, "name":"African Hunting Dog", "life_span":"11 years", "image":{"id":"rkiByec47", "width":500, "height":335, "url":"https://cdn2.thedogapi.com/images/rkiByec47.jpg"}, "temperament":"Wild, Hardworking, Dutiful", "origin":"", "weight":{"imperial":"44 - 66", "metric":"20 - 30"}, "bred_for":"A wild pack animal", "reference_image_id":"rkiByec47", "height":{"imperial":"30", "metric":"76"}}]

Not quite, because if you look closely, you’ll notice that the order of the attributes has changed. First come the attributes that were defined in the record, and then all the others that the API returns to us. Now here comes the open record mentioned above into play. It simply includes additional attributes as type any. If we had defined a closed record, we would have gotten an error here because there are attributes that cannot be mapped. A closed record is defined like an open record, but there is a pipe after the curly brace

1
2
3
4
5
type Breed readonly & record {|
    int id;
    string name;
    string life_span;
|};

The response from the service would then have been the following:

1
{"timestamp":"2024-03-07T17:35:40.821494Z", "status":500, "reason":"Internal Server Error", "message":"Payload binding failed: 'json[]' value cannot be converted to 'hello_world:Breed[]': \n\t\tfield '[0].weight' cannot be added to the closed record 'hello_world:(swelsch\/hello_world:0:$anonType$Breed$_0 & readonly)'\n\t\tfield '[0].height' cannot be added to the closed record 'hello_world:(swelsch\/hello_world:0:$anonType$Breed$_0 & readonly)'\n\t\tfield '[0].bred_for' cannot be added to the closed record 'hello_world:(swelsch\/hello_world:0:$anonType$Breed$_0 & readonly)'\n\t\tfield '[0].breed_group' cannot be added to the closed record 'hello_world:(swelsch\/hello_world:0:$anonType$Breed$_0 & readonly)'\n\t\tfield '[0].temperament' cannot be added to the closed record 'hello_world:(swelsch\/hello_world:0:$anonType$Breed$_0 & readonly)'\n\t\tfield '[0].origin' cannot be added to the closed record 'hello_world:(swelsch\/hello_world:0:$anonType$Breed$_0 & readonly)'\n\t\tfield '[0].reference_image_id' cannot be added to the closed record 'hello_world:(swelsch\/hello_world:0:$anonType$Breed$_0 & readonly)'\n\t\tfield '[0].image' cannot be added to the closed record 'hello_world:(swelsch\/hello_world:0:$anonType$Breed$_0 & readonly)'\n\t\tfield '[1].weight' cannot be added to the closed record 'hello_world:(swelsch\/hello_world:0:$anonType$Breed$_0 & readonly)'\n\t\tfield '[1].height' cannot be added to the closed record 'hello_world:(swelsch\/hello_world:0:$anonType$Breed$_0 & readonly)'\n\t\tfield '[1].country_code' cannot be added to the closed record 'hello_world:(swelsch\/hello_world:0:$anonType$Breed$_0 & readonly)'\n\t\tfield '[1].bred_for' cannot be added to the closed record 'hello_world:(swelsch\/hello_world:0:$anonType$Breed$_0 & readonly)'\n\t\tfield '[1].breed_group' cannot be added to the closed record 'hello_world:(swelsch\/hello_world:0:$anonType$Breed$_0 & readonly)'\n\t\tfield '[1].temperament' cannot be added to the closed record 'hello_world:(swelsch\/hello_world:0:$anonType$Breed$_0 & readonly)'\n\t\tfield '[1].origin' cannot be added to the closed record 'hello_world:(swelsch\/hello_world:0:$anonType$Breed$_0 & readonly)'\n\t\tfield '[1].reference_image_id' cannot be added to the closed record 'hello_world:(swelsch\/hello_world:0:$anonType$Breed$_0 & readonly)'\n\t\tfield '[1].image' cannot be added to the closed record 'hello_world:(swelsch\/hello_world:0:$anonType$Breed$_0 & readonly)'\n\t\tfield '[2].weight' cannot be added to the closed record 'hello_world:(swelsch\/hello_world:0:$anonType$Breed$_0 & readonly)'\n\t\tfield '[2].height' cannot be added to the closed record 'hello_world:(swelsch\/hello_world:0:$anonType$Breed$_0 & readonly)'\n\t\tfield '[2].bred_for' cannot be added to the closed record 'hello_world:(swelsch\/hello_world:0:$anonType$Breed$_0 & readonly)'\n\t\t...", "path":"/breeds_record", "method":"GET"}

Ok, let’s take care of writing to the database. For this, we create a new file dynamob.bal in which we want to encapsulate our logic for writing to the database.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import ballerinax/aws.dynamodb;
import ballerina/os;
import ballerina/log;

dynamodb:ConnectionConfig amazonDynamodbConfig = {
    awsCredentials: {
        accessKeyId: os:getEnv("ACCESS_KEY_ID"),
        secretAccessKey: os:getEnv("SECRET_ACCESS_KEY")
    },
    region: os:getEnv("AWS_DEFAULT_REGION")
};

dynamodb:Client amazonDynamodbClient = check new (amazonDynamodbConfig);

function persist(Breed[] breeds) returns boolean|error {
    foreach var breed in breeds {
        dynamodb:ItemCreateInput createItemInput = {
            tableName: "dogs",
            item: {
                "Id": {"N": breed.id.toString()},
                "Name": {"S": breed.name},
                "Lifespan": {"S": breed.life_span}
            }
        };
        dynamodb:ItemDescription response = check amazonDynamodbClient->createItem(createItemInput);
        log:printInfo(response.toString());
    }
    return true;
}

[!NOTE]

Unfortunately, there is currently no way to use a “local” DynamoDB. In the code of the Dynamodb client, the AWS host is set fixed.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public isolated function init(ConnectionConfig config) returns error? {
        self.accessKeyId = config.awsCredentials.accessKeyId;
        self.secretAccessKey = config.awsCredentials.secretAccessKey;
        self.securityToken = config.awsCredentials?.securityToken;
        self.region = config.region;
        self.awsHost = AWS_SERVICE + DOT + self.region + DOT + AWS_HOST;
        string endpoint = HTTPS + self.awsHost;

        http:ClientConfiguration httpClientConfig = check config:constructHTTPClientConfig(config);
        self.awsDynamoDb = check new (endpoint, httpClientConfig);
    }

Ok, let’s take a look at what’s happening here. In lines 4-12, the DynamoDB client is created with the required data. I simply pulled these as environment variables, since I use tlr.dev in the setup. A tool that I can really recommend if you want to manage secrets for local development. I also wrote a corresponding Techup about it almost 2 years ago.

Then, in lines 15-29, we implement our method for storing the dog breed. I think the code is self-explanatory, so I’ll spare you the explanation at this point :-).

In the main.bal, we now simply call the new method

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    resource function get breeds_and_persist() returns anydata|error? {
        http:Client jcClient = check new ("https://api.thedogapi.com/");

        Breed[] breeds = check jcClient->/v1/breeds('limit = 3, has_breeds=1, headers = {
            "x-api-key": os:getEnv("THE_DOG_API_KEY")
        });

        var result = check persist(breeds);

        return result;
    }

That’s it. We now have a complete application that writes data from a web service to a DynamoDB. Code-wise, I find it very simple and intuitive to understand.

Ballerina Visual Studio Code UI

But let’s take a look at another highlight that I already mentioned above. The Visual Studio Code extension comes with a UI, which we want to take a quick look at now.

image-20240307201131770

In the upper right corner, there is a symbol with which we can display the diagram. When you click on it, an overview of all components opens first.

image-20240307201254321

We can now take a closer look at the corresponding component by clicking on it and even edit it via the UI. Let’s take a closer look at the function.

image-20240307201426817

We see in the representation what exactly happens in the persist function in terms of the process flow. This way, you always have a nice visual representation of the written code and can also see when the communication with the DynamoDB happens. We can now click on one of the boxes to edit the code directly. When we click on the box in the middle, i.e. where “createItem” is called, we see the following window:

image-20240307202000662

Let’s also take a look at the service. By clicking on the house in the upper left corner, we get back to the component overview. There we now click on the service / and see all endpoints that are registered in our application. I have created several endpoints for testing, so I have 4 endpoints in my view.

image-20240307202238759

The UI feature is quite nice, but as a code enthusiast, I still prefer it in text form. I always find clicking around very tedious, and in my opinion, you’re also much faster when you write “good old code”. In this sense, greetings to all NoCode and LowCode enthusiasts ;-)

Conclusion

How will b-nova continue with Ballerina? Well, we will try out different scenarios with Ballerina in a half-day hackathon, as it is really exciting, especially for small projects. Also exciting for us is what a CI/CD pipeline with Ballerina looks like and how the application performs under load. So if you want to learn more about Ballerina, you will definitely be able to learn one or two things from us, so “Stay Tuned :rocket: “!

You can find the entire project in our Techup Github.

This techup has been translated automatically by Gemini

Stefan Welsch

Stefan Welsch – Pionier, Stuntman, Mentor. Als Gründer von b-nova ist Stefan immer auf der Suche nach neuen und vielversprechenden Entwicklungsfeldern. Er ist durch und durch Pragmatiker und schreibt daher auch am liebsten Beiträge die sich möglichst nahe an 'real-world' Szenarien anlehnen.