With the emergence of microservices architecture, applications are developed by using a large number of smaller programs. These programs are built individually and deployed into a platform where they can scale independently. These programs communicate with each other over the network through simple Application Programming Interfaces (APIs). With the disaggregated and network distributed nature of these applications, developers have to deal with the Fallacies of Distributed Computing as part of their application logic.
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. To consume these APIs, in the client-side, the developer has to implement different resilient techniques like retry, failover, and load balancing to deal with unreliable network behavior.
Ballerina, an open-source programming language, introduces fundamental, new abstractions of client objects, services, resource functions, and listeners to bring networking into the language. Developers can use the language-provided constructs to write these network programs and it just works.
Ballerina introduces service typing where services can have one or more resource methods in which the application logic is implemented. A service can work in conjunction with a listener object. A listener object provides an interface between the network and the service. It receives network messages from a remote process according to the defined protocol and translates it into calls on the resource methods of the service that has been attached to the listener object. HTTP/HTTPS, HTTP2, gRPC, and WebSockets are some of the protocols that are supported out-of-the-box in the listener object.
The following code illustrates a hello service that has sayHello resource methods which returns “Hello World!”.
import ballerina/http;
import ballerina/log;
listener http:Listener helloWorldEP = new(9090);
service hello on helloWorldEP {
resource function sayHello(http:Caller caller,
http:Request request) {
var result = caller->respond("Hello World!");
if (result is error) {
log:printError("Error in responding ", err = result);
}
}
}
Ballerina source file can compile into an executable jar.
$ ballerina build hello.bal
Compiling source
hello.bal
Generating executables
Hello.jar
$ java -jar hello.jar
[ballerina/http] started HTTP/WS listener 0.0.0.0:9090
$ curl http://localhost:9090/hello/sayHello
Hello, World!
Ballerina service comes 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 Ballerina by Examples (BBEs) show different protocol support in the listener object:
In request-response paradigm network communication is done via blocking calls. But blocking a thread to a network call is very expensive. Because of that, programing languages supported asyncio and developers have to implement async/await by using callback-based code techniques. Ballerina request-response protocols are out-of-the-box non-blocking and underneath the Ballerina protocol implementation will take care of the asynchronous invocation. The following code snippet shows a call to a simple HTTP GET request endpoint where the Ballerina HTTP client protocol implementation will handle the non-blocking nature of the underneath invocation.
import ballerina/http;
import ballerina/io;
http:Client clientEndpoint = new("http://postman-echo.com");
public function main() {
var response = clientEndpoint->get("/get?test=123");
if (response is http:Response) {
// logic for handle response
} else {
io:println("Error when calling the backend: ", response.reason());
}
}
A client object is a stub that allows a worker to send network messages to a remote process according to some protocol. The remote methods of the client object correspond to distinct network messages defined by the protocol for the role played by the client object.
The following sample illustrates sending out a tweet by invoking tweet remote method in the twitter client object.
import ballerina/config;
import ballerina/log;
import wso2/twitter;
// Twitter package defines this type of endpoint
// that incorporates the twitter API.
// We need to initialize it with OAuth data from apps.twitter.com.
// Instead of providing this confidential data in the code
// we read it from a toml file.
twitter:Client twitterClient = new ({
clientId: config:getAsString("clientId"),
clientSecret: config:getAsString("clientSecret"),
accessToken: config:getAsString("accessToken"),
accessTokenSecret: config:getAsString("accessTokenSecret"),
clientConfig: {}
});
public function main() {
twitter:Status|error status = twitterClient->tweet("Hello World!");
if (status is error) {
log:printError("Tweet Failed", status);
} else {
log:printInfo("Tweeted: " + <@untainted>status.id.toString());
}
}
A program sends a message by calling a remote method (tweet) by using the protocol defined in the client object (twitterClient). The return value (twitter:Status) corresponds to the protocol's response.
A sequence diagram is the best way to visually describe how services interact. This was the foundation for designing the syntax and semantics of the Ballerina language abstractions for concurrency and network interaction so that it has a close correspondence to sequence diagrams. In Ballerina, a remote method is invoked using a different syntax (->) from a non-remote method and it is depicted as a horizontal arrow from the worker’s lifeline to the client’s object lifeline in a sequence diagram.
Additionally, this is a bidirectional mapping between the textual representation of code in Ballerina syntax and the visual representation as a sequence diagram. The Ballerina IDE plugin (for example, the VSCode plugin) can generate a sequence diagram dynamically from the source code.
The following diagram is generated from a sample microservice of Salesforce integration.
Because of the unreliable nature of the network, network programs need to be written in a way that handles failures. Sometimes an automatic retry will help recover from such failures. In some cases, failover techniques will help to have uninterrupted service delivery. Also, techniques like circuit breakers help to prevent catastrophic cascading failure across multiple programs and help to recover failed programs. Resilient techniques like a circuit breaker, load balance, failover, retry, and timeout comes out-of-the-box in the Ballerina’s client object.
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://localhost:8080", {
circuitBreaker: {
rollingWindow: {
timeWindowInMillis: 10000,
bucketSizeInMillis: 2000,
requestVolumeThreshold: 0
},
failureThreshold: 0.2,
resetTimeInMillis: 10000,
statusCodes: [400, 404, 500]
},
timeoutInMillis: 2000
});
The following BBEs exhibit working samples of different resilience technique support in Ballerina.
Due to the inherent unreliability of networks, errors are an expected part of network programming. Ballerina's approach is to explicitly check for errors rather than throw them as exceptions. It's pretty much impossible to ignore errors by design. Ballerina has a built-in error type that has two components: a string identifying the reason for the error and a mapping giving additional details about the error.
Let’s take the same tweet example that we discussed above.
twitter:Status|error status = twitterClient->tweet("Hello World!");
if (status is error) {
log:printError("Tweet Failed", status);
} else {
log:printInfo("Tweeted: " + <@untainted>status.id.toString());
}
The tweet remote method can return the expected twitter:Status value or an error due to network unreliability. Ballerina supports union types so the status variable can be either twitter:Status or error type. The above code snippet shows how to explicitly check errors. Also the Ballerina IDE tools support type guard where it guides developers to handle errors and values correctly in the if-else block.
The following BBEs demonstrate comprehensive error handling capabilities in Ballerina.
Distributed systems work by sharing data between different components. Network security plays a crucial role because all these communications happen over the network. Ballerina provides built-in libraries to implement transport-level security and cryptographic libraries to protect data. The following BBE shows Ballerina’s cryptographic operation support.
Identity and access management plays a critical role in microservice-based applications. In general, for initial login, typically users provide the username/ID and password. But on subsequent access a cookie or randomized token is used as proof of identity. Ballerina supports out-of-the-box protection for services as well as clients by using basic-auth, OAuth and JWT. The following BBEs show how to secure services and clients by enforcing authorization.
Ballerina http:Listener and http:Client has out-of-the-box support for verification of certificate revocation through OCSP (Online Certificate Status Protocol) stapling, OCSP and CRL. Ballerina listeners and client objects support all major functionality needed in network programming for end-to-end protection for network communication.
In addition to these, Ballerina has an built-in taint analyzer in its compiler. Taint analysis is designed to increase security by preventing any variable that can be modified by user input. All user input can be dangerous if this isn't properly checked. As a result of the taint analysis mechanism, the Ballerina compiler identifies 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. The following BBE show taint checking support of Ballerina.
Increasing the number of smaller programs means debugging an issue will be harder and require extra effort for enabling observability on all distributed programs. Monitoring, logging, and distributed tracing are key methods that reveal the internal state of the system and provide observability. Ballerina becomes fully observable by exposing itself via these three methods to various external systems.
This helps with monitoring metrics such as request count and response time statistics, analyzing logs, and performing distributed tracing. More insights into Ballerina observability can be found in the following guide:
Ballerina’s goal is to be a programming language and a platform co-designed to make enterprise integration simpler, more agile and DevOps friendly by including cloud-native and middleware abstractions in addition to the expected general-purpose functionality.
In the near future, the network aspect of the language will be enhanced by adding support for transactions including distributed transactions between Ballerina programs, streaming query, and locking.