We need a technology that provides the best of both worlds. It needs to have the flexibility we get from a general-purpose programming language, while also having the features to model solutions based on higher-level abstractions. Especially in the current cloud-era, we need technologies that can model distributed systems in a more developer-friendly manner. A software system with multiple actors, concurrent execution flows, and remote endpoints are some of the things we need to handle in these types of dynamic environments.
Use Case: Bank ATM Operations
Let’s take a look at a situation where we need to design a software system with multiple participants who each represent a certain functionality of the system. For a single use case, we need to model a flow that shows how all the participants are involved. The most natural way for us to represent this would be to use a sequence diagram. Figure 1 shows a scenario of modeling the functionality of a bank ATM cash withdrawal operation.
Figure 1: Bank ATM Withdrawal Sequence Diagram
The “Client” is the external actor that triggers the interactions between the participants in the system. The ATM Service would be a network service that takes in requests from the client and connects to the core banking system, which in turn interacts with a database for storing and retrieving account information.
Let’s take a look at how we can use this approach in the Ballerina programming language. Here we will use a code-first approach in defining the execution flows and because the syntax is fully compatible with sequence diagramming constructs we can automatically visualize the code we wrote as a sequence diagram.
The Ballerina Way
The above ATM operations scenario can be implemented in code using Ballerina as shown in Figure 2.
Figure 2: Ballerina ATM Service Code in VS Code
In the top right corner, we have the “Show File Overview” option which opens the following panel when clicked on.
Figure 3: Ballerina ATM Service Model Visualized in VS Code
The above image shows the sequence diagram that is generated from the skeleton code we have implemented earlier. The entry point is the service resource function. The actor is shown as the “caller”, which represents the client who is invoking the service resource. The “Default” participant is the resource function itself, showing the operations in its lifeline. From the resource function, further function calls such as checkBalance() and debitAccount() calls are shown in its lifeline, and their internal operations are further expanded and merged to the same sequence diagram to show their operations as well.
Client Objects and Remote Methods
In these sections, we can see another participant in the sequence diagram — “accountDB” — which represents the database we are interacting with. In Ballerina, these are special network client objects that have their own lifeline to represent its functionality and the messages that it can receive. The messages sent to these network clients or the invocations done on them are called remote methods. A remote method is a special method inside a client object that represents a call through the network. The difference between a normal method call and a remote call is distinguished by using the arrow “->” notation to call the remote methods.
For example, in our previous sample, the following line represents the client action invocation done on the database client to retrieve database records.
var selectRet = check accountsDB->select("SELECT balance FROM Account WHERE id = ?", Account, id);
Earlier we talked about the “caller” being the actor that called into our service resource. This is actually the name of the first parameter of our resource method, which also happens to be a client object that represents the caller of our service resource. The following code shows the declaration of the resource function definition.
resource function withdrawMoney(http:Caller caller, http:Request request, string id, decimal val) returns @tainted error?
The caller instance of type http:Caller, being a client object in Ballerina, can be used by the resource author to communicate back with the caller by invoking its remote methods. By using this model, we can easily implement messaging patterns such as bi-directional communication and have multiple interactions with the caller rather than only returning the result back to the caller at the end of the resource method definition. The line below shows how we responded to the client using the caller instance.
var result = check caller->respond("Fail: no funds");
The result here represents the response retrieved back from the caller, where for example, if there was an error communicated back to the caller, we can perform further actions in our resource method rather than ignoring it. This pattern encourages a more robust approach to handling errors.
The concurrency model in Ballerina is made using the concept of a strand. A strand is a lightweight thread where a single operating system thread can contain multiple strands. A single strand is run at a time in a thread, and the strands belonging to a single thread are cooperatively multitasked. This is especially beneficial in implementing non-blocking I/O operations in Ballerina where strands allow us to optimize the usage of the CPU time.
A worker is the construct we use in Ballerina to represent a single strand execution in a function. A single function can have multiple workers in order to define the concurrent operations in the function. The function shown in Figure 4 below is an update to our earlier scenario, where we do some operations to initiate the system.
Figure 4: Ballerina ATM Service Initiation Code in VS Code
In the code, we are contacting a couple of network endpoints, retrieving information and reporting some state at the end. The network operations are mixed in with some computational code that is required to do some calculations. Even though our initSystem function will be run in a single thread, the execution is optimized with the internal use of strands. While the I/O operations are happening with the HTTP network calls, the user-space scheduler would have switched the physical execution of the thread to the other worker to get the processing done.
Let’s take a look at how Ballerina’s concurrency execution is visualized in the sequence diagram view. The diagram view of the initSystem function is shown below in Figure 5.
Figure 5: Ballerina ATM Service Initiation Model Visualized in VS Code
Here we can see the workers have become participants in the sequence diagram alongside the HTTP clients. The workers’ activations as seen in their lifelines are occurring concurrently and any communication between the workers is done using message passing, using Ballerina’s send (->) and receive (<-) actions. Also, during the message passing scenarios between workers, the compiler does explicit checks to verify that the send and receive actions are in a consistent state in order to avoid any deadlock scenarios in the runtime.
In this manner, we can easily model concurrent executions and their communication channels and visualize them conveniently using sequence diagrams. This approach to programming provides self-documentation for the code we write and overall improved productivity.
We have seen how Ballerina is designed in a way that it is always possible to generate the program’s representation as a sequence diagram in a meaningful way. This behavior is seen in the language’s special constructs for networking clients, listeners, concurrency constructs, message parsing, and many other aspects.