Data-oriented programming with Ballerina:
A comparative analysis
Data-oriented programming is crucial in modern software development due to the complex and data-intensive nature of applications and the widespread adoption of microservices architecture, emphasizing the need for efficient data handling.
With its robust support, seamless integration with data constructs, and powerful features make Ballerina the top choice for efficient data handling and processing.
Model data as data
Data-oriented programming encourages representing data in its pure form. Ballerina and Java provide records, which is a language construct that simplifies this pure data representation. While Java has recently enhanced its capabilities to support this approach, Ballerina has been fundamentally architected to facilitate data-oriented programming from its inception.
Java
enum UserType {
ADMIN,
CUSTOMER,
GUEST
}
record User(int id, String name, UserType userType) {
public User(int id, String name) {
this(id, name, UserType.GUEST);
}
}
class Main {
public static void main(String[] args) {
User customer = new User(1, "John Doe");
System.out.printf("User '%s' with id '%s' as '%s' created successfully",
customer.name(), customer.id(), customer.userType());
}
}
Ballerina
import ballerina/io;
enum UserType {
ADMIN,
GUEST,
MEMBER
};
type User record {|
int id;
string name;
UserType userType = GUEST;
|};
public function main() {
User user = {id: 1, name: "John Doe"};
io:println(string `User '${user.name}' with id '${user.id}' as '${user.userType
}' created successfully`);
}
enum UserType {
ADMIN,
CUSTOMER,
GUEST
}
record User(int id, String name, UserType userType) {
public User(int id, String name) {
this(id, name, UserType.GUEST);
}
}
class Main {
public static void main(String[] args) {
User customer = new User(1, "John Doe");
System.out.printf("User '%s' with id '%s' as '%s' created successfully",
customer.name(), customer.id(), customer.userType());
}
}
import ballerina/io;
enum UserType {
ADMIN,
GUEST,
MEMBER
};
type User record {|
int id;
string name;
UserType userType = GUEST;
|};
public function main() {
User user = {id: 1, name: "John Doe"};
io:println(string `User '${user.name}' with id '${user.id}' as '${user.userType
}' created successfully`);
}
Model choices as discriminate unions
Modeling choices play a crucial role in achieving code-data separation in data-oriented programming, leading to modular, maintainable, and extensible code that can handle diverse data variants in a unified and type-safe manner.
Both Java and Ballerina provide mechanisms to model choices as discriminate unions. Java uses interfaces or abstract classes along with class hierarchies and method overrides to represent the variants and their behaviors. On the other hand, Ballerina offers built-in support for discriminate unions with a concise and language-integrated syntax.
Java
sealed interface Shape permits Circle, Rectangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}
class Main {
public static double calculateArea(Shape shape) {
switch (shape) {
case Circle circle:
return Math.PI * circle.radius() * circle.radius();
case Rectangle rectangle:
return rectangle.width() * rectangle.height();
}
}
public static void main(String[] args) {
System.out.println(calculateArea(new Circle(10)));
}
}
Ballerina
import ballerina/io;
type Circle record {|
float radius;
|};
type Rectangle record {|
float width;
float height;
|};
type Shape Circle|Rectangle;
function calculateArea (Shape shape) returns float {
if shape is Circle {
return float:PI * shape.radius * shape.radius;
}
return shape.width * shape.height;
};
public function main() {
io:println(calculateArea({radius: 10}));
}
sealed interface Shape permits Circle, Rectangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}
class Main {
public static double calculateArea(Shape shape) {
switch (shape) {
case Circle circle:
return Math.PI * circle.radius() * circle.radius();
case Rectangle rectangle:
return rectangle.width() * rectangle.height();
}
}
public static void main(String[] args) {
System.out.println(calculateArea(new Circle(10)));
}
}
import ballerina/io;
type Circle record {|
float radius;
|};
type Rectangle record {|
float width;
float height;
|};
type Shape Circle|Rectangle;
function calculateArea (Shape shape) returns float {
if shape is Circle {
return float:PI * shape.radius * shape.radius;
}
return shape.width * shape.height;
};
public function main() {
io:println(calculateArea({radius: 10}));
}
Model optionality
In data-oriented programming, where data is at the forefront, modeling optionality provides a powerful mechanism to express the presence or absence of data in a concise and type-safe manner.
Optional typing allows indicating when a value may be absent or nullable, while optional fields provide flexibility in representing varying data states.
Ballerina has built-in support for optional types and fields, eliminating the risk of null pointer exceptions and related bugs. In Java, handling optional types and fields typically involves using external libraries or annotations, which can introduce additional complexity and potential for errors.
Java
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
record Person(int id, String name, Integer age, String email, List<String> availableFields) {
public Optional<String> getEmail() {
return Optional.ofNullable(email);
}
public Optional<Integer> getAge() {
return Optional.ofNullable(age);
}
}
class Main {
public static void main(String[] args) throws JsonProcessingException {
String jsonInput = """
{
"id": 1,
"name": "John Doe",
"age": null
}
""";
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addDeserializer(Person.class, new PersonDeserializer());
objectMapper.registerModule(module);
Person person = objectMapper.readValue(jsonInput, Person.class);
System.out.println(person.age()); // output: null
// optional type access
int age = person.getAge().orElse(-1);
System.out.println(age); // output: -1
// optional field access
System.out.println(person.availableFields().contains("email")); // output: false
String emailValue = person.getEmail().isPresent()
? person.email()
: "Email is not provided";
System.out.println(emailValue); // output: Email is not provided
}
}
class PersonDeserializer extends StdDeserializer<Person> {
public PersonDeserializer() {
this(null);
}
public PersonDeserializer(Class<?> vc) {
super(vc);
}
@Override
public Person deserialize(JsonParser jsonParser,
DeserializationContext deserializationContext) throws IOException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
List<String> availableFields = Arrays.asList("name", "age", "id");
int id = node.get("id").asInt();
String name = node.get("name").asText();
Integer age = node.get("age").asInt();
String email = null;
if (node.has("email")) {
email = node.get("email").asText();
availableFields.add("email");
}
return new Person(id, name, age, email, availableFields);
}
}
Ballerina
import ballerina/io;
type Person record {|
int id;
string name;
// optional typed field
int? age;
// optional field
string email?;
|};
public function main() returns error? {
json jsonInput = {
id: 1,
"name": "John Doe",
"age": null
};
Person person = check jsonInput.fromJsonWithType();
io:println(person.age.toBalString()); // output: ()
// optional type access
int age = person.age ?: -1;
io:println(age); // output: -1
// optional field access
io:println(person.hasKey("email")); // output: false
string email = person.email ?: "Email is not provided";
io:println(email); // output: Email is not provided
}
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
record Person(int id, String name, Integer age, String email, List<String> availableFields) {
public Optional<String> getEmail() {
return Optional.ofNullable(email);
}
public Optional<Integer> getAge() {
return Optional.ofNullable(age);
}
}
class Main {
public static void main(String[] args) throws JsonProcessingException {
String jsonInput = """
{
"id": 1,
"name": "John Doe",
"age": null
}
""";
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addDeserializer(Person.class, new PersonDeserializer());
objectMapper.registerModule(module);
Person person = objectMapper.readValue(jsonInput, Person.class);
System.out.println(person.age()); // output: null
// optional type access
int age = person.getAge().orElse(-1);
System.out.println(age); // output: -1
// optional field access
System.out.println(person.availableFields().contains("email")); // output: false
String emailValue = person.getEmail().isPresent()
? person.email()
: "Email is not provided";
System.out.println(emailValue); // output: Email is not provided
}
}
class PersonDeserializer extends StdDeserializer<Person> {
public PersonDeserializer() {
this(null);
}
public PersonDeserializer(Class<?> vc) {
super(vc);
}
@Override
public Person deserialize(JsonParser jsonParser,
DeserializationContext deserializationContext) throws IOException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
List<String> availableFields = Arrays.asList("name", "age", "id");
int id = node.get("id").asInt();
String name = node.get("name").asText();
Integer age = node.get("age").asInt();
String email = null;
if (node.has("email")) {
email = node.get("email").asText();
availableFields.add("email");
}
return new Person(id, name, age, email, availableFields);
}
}
import ballerina/io;
type Person record {|
int id;
string name;
// optional typed field
int? age;
// optional field
string email?;
|};
public function main() returns error? {
json jsonInput = {
id: 1,
"name": "John Doe",
"age": null
};
Person person = check jsonInput.fromJsonWithType();
io:println(person.age.toBalString()); // output: ()
// optional type access
int age = person.age ?: -1;
io:println(age); // output: -1
// optional field access
io:println(person.hasKey("email")); // output: false
string email = person.email ?: "Email is not provided";
io:println(email); // output: Email is not provided
}
Be conservative in what you send, be liberal in what you accept
Ballerina employs "be conservative in what you send, be liberal in what you accept" by using structural types that support openness.
These types serve a dual purpose: enhancing static typing within programs and describing service interfaces accurately. While outgoing messages are tightly controlled to ensure protocol adherence, incoming data is handled with a degree of flexibility. The result is a balance of strictness and tolerance that enhances interoperability and resilience. This makes Ballerina a robust and adaptable choice for constructing cloud-native applications.
Java
import java.util.HashMap;
import java.util.Map;
final class PersonalDetails {
String name;
int age;
PersonalDetails(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "PersonalDetails{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
final class EmployeeDetails {
String designation;
float salary;
Map<String, Object> otherFields;
EmployeeDetails(String designation, float salary, Map<String, Object> otherFields) {
this.designation = designation;
this.salary = salary;
this.otherFields = otherFields;
}
@Override
public String toString() {
return "EmployeeDetails{" +
"designation='" + designation + '\'' +
", salary=" + salary +
", otherFields=" + otherFields +
'}';
}
};
class Main {
public static void main(String[] args) {
// Create a new PersonalDetails populating defined fields
PersonalDetails personalDetails = new PersonalDetails("John Doe", 30);
// Access and modify defined fields
personalDetails.name = "Jane Smith";
personalDetails.age = personalDetails.age + 1;
// Create a new EmployeeDetails with dynamic fields
EmployeeDetails employeeInfo = new EmployeeDetails("n/a", 3000.0f,
convertToMap(personalDetails));
// Access and modify defined fields
employeeInfo.designation = "Software Engineer";
employeeInfo.salary = 5000.0f;
// Access and modify dynamically added fields
employeeInfo.otherFields.put("name", "John Smith");
// Add a new dynamic field
employeeInfo.otherFields.put("address", "123 Main St");
// Print the updated data
System.out.println(personalDetails);
System.out.println(employeeInfo);
}
static Map<String, Object> convertToMap(Object obj) {
Map<String, Object> map = new HashMap<>();
for (var field : obj.getClass().getDeclaredFields()) {
try {
map.put(field.getName(), field.get(obj));
} catch (IllegalAccessException ignored) {
}
}
return map;
}
}
Ballerina
import ballerina/io;
// closed record
type PersonalDetails record {|
string name;
int age;
|};
// open record
type EmployeeDetails record {
string designation;
float salary;
};
public function main() {
// Create a new employee record with closed fields
PersonalDetails personalDetails = {name: "John Doe", age: 30};
// Access and modify closed record fields using dot notation
personalDetails.name = "Jane Smith";
personalDetails.age = personalDetails.age + 1;
// Create a new employee record with open and closed fields
EmployeeDetails employeeInfo = {designation: "n/a", salary: 3000.0, ...personalDetails};
// Access and modify open record fields using dot notation
employeeInfo.designation = "Software Engineer";
employeeInfo.salary = 5000.0;
// Access and modify record fields using bracket notation
employeeInfo["name"] = "John Smith";
// Add a new field to the employee record
employeeInfo["address"] = "123 Main St";
// Print the updated employee information
io:println(personalDetails);
io:println(employeeInfo);
}
import java.util.HashMap;
import java.util.Map;
final class PersonalDetails {
String name;
int age;
PersonalDetails(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "PersonalDetails{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
final class EmployeeDetails {
String designation;
float salary;
Map<String, Object> otherFields;
EmployeeDetails(String designation, float salary, Map<String, Object> otherFields) {
this.designation = designation;
this.salary = salary;
this.otherFields = otherFields;
}
@Override
public String toString() {
return "EmployeeDetails{" +
"designation='" + designation + '\'' +
", salary=" + salary +
", otherFields=" + otherFields +
'}';
}
};
class Main {
public static void main(String[] args) {
// Create a new PersonalDetails populating defined fields
PersonalDetails personalDetails = new PersonalDetails("John Doe", 30);
// Access and modify defined fields
personalDetails.name = "Jane Smith";
personalDetails.age = personalDetails.age + 1;
// Create a new EmployeeDetails with dynamic fields
EmployeeDetails employeeInfo = new EmployeeDetails("n/a", 3000.0f,
convertToMap(personalDetails));
// Access and modify defined fields
employeeInfo.designation = "Software Engineer";
employeeInfo.salary = 5000.0f;
// Access and modify dynamically added fields
employeeInfo.otherFields.put("name", "John Smith");
// Add a new dynamic field
employeeInfo.otherFields.put("address", "123 Main St");
// Print the updated data
System.out.println(personalDetails);
System.out.println(employeeInfo);
}
static Map<String, Object> convertToMap(Object obj) {
Map<String, Object> map = new HashMap<>();
for (var field : obj.getClass().getDeclaredFields()) {
try {
map.put(field.getName(), field.get(obj));
} catch (IllegalAccessException ignored) {
}
}
return map;
}
}
import ballerina/io;
// closed record
type PersonalDetails record {|
string name;
int age;
|};
// open record
type EmployeeDetails record {
string designation;
float salary;
};
public function main() {
// Create a new employee record with closed fields
PersonalDetails personalDetails = {name: "John Doe", age: 30};
// Access and modify closed record fields using dot notation
personalDetails.name = "Jane Smith";
personalDetails.age = personalDetails.age + 1;
// Create a new employee record with open and closed fields
EmployeeDetails employeeInfo = {designation: "n/a", salary: 3000.0, ...personalDetails};
// Access and modify open record fields using dot notation
employeeInfo.designation = "Software Engineer";
employeeInfo.salary = 5000.0;
// Access and modify record fields using bracket notation
employeeInfo["name"] = "John Smith";
// Add a new field to the employee record
employeeInfo["address"] = "123 Main St";
// Print the updated employee information
io:println(personalDetails);
io:println(employeeInfo);
}
Declarative data processing
Ballerina's query language is a powerful feature that enhances data-oriented programming by providing a concise and expressive way to transform and manipulate data. It allows developers to perform complex data operations such as filtering, mapping, aggregating, and sorting with ease. The query language in Ballerina is specifically designed to work seamlessly with structured data types like records, making it well-suited for data-oriented programming tasks.
import ballerina/http;
import ballerina/io;
type Country record {
string country;
int population;
string continent;
int cases;
int deaths;
};
// Prints the top 10 countries having the highest case-fatality ratio grouped by continent.
public function main() returns error? {
http:Client diseaseEp = check new ("https://disease.sh/v3");
Country[] countries = check diseaseEp->/covid\-19/countries;
json summary =
from var {country, continent, population, cases, deaths} in countries
where population >= 100000 && deaths >= 100
let decimal caseFatalityRatio = (<decimal>deaths / <decimal>cases * 100).round(4)
let json countryInfo = {country, population, caseFatalityRatio}
order by caseFatalityRatio descending
limit 10
group by continent
order by avg(caseFatalityRatio)
select {continent, countries: [countryInfo]};
io:println(summary);
}
Pattern matching
Pattern matching empowers developers to effortlessly extract pertinent data from intricate patterns and execute precise operations based on the data's structure and content. Both Ballerina and Java offer the ability to handle complex data structures concisely and expressively using pattern matching techniques.
Java
import java.util.Map;
class Main {
static final String switchStatus = "ON";
public static String matchValue(Object value, boolean isObstructed,
float powerPercentage) {
switch (value) {
case Integer i -> {
if (i == 1 && !isObstructed) {
return "Move forward";
}
if (i == 2 || i == 3) {
return "Turn";
}
if (i == 4 && powerPercentage > 25.0) {
return "Increase speed";
}
return "Invalid instruction";
}
case String str -> {
if (str.equals("STOP")) {
return "STOP";
}
if (str.equals(switchStatus)) {
return "Switch ON";
}
return "Invalid instruction";
}
case double[] arr -> {
if (arr.length == 2) {
return "Maneuvering to x: " + arr[0]
+ " and y: " + arr[1] + " coordinates";
} else {
return "Invalid instruction";
}
}
case Record record -> {
double a = record.x;
double b = record.y;
Map<String, Object> rest = record.rest;
String optionalArg = matchValue(rest, isObstructed, powerPercentage);
return "Maneuvering to x: " + a + " and y: " + b +
" coordinates with " + optionalArg;
}
default -> {
return "Invalid instruction";
}
}
}
record Record(double x, double y, Map<String, Object> rest) {}
public static void main(String[] args) {
String output = matchValue(new double[] { 2.516, 51.409 }, false, 0.0f);
System.out.println(output);
}
}
Ballerina
import ballerina/io;
const switchStatus = "ON";
function matchValue(anydata value, boolean isObstructed,
float powerPercentage) returns string {
// The value of the `val` variable is matched against the given value match patterns.
match value {
1 if !isObstructed => {
// This block will execute if `value` is 1 and `isObstructed` is false.
return "Move forward";
}
// `|` is used to match more than one value.
2|3 => {
// This block will execute if `value` is either 2 or 3.
return "Turn";
}
4 if 25.0 < powerPercentage => {
// This block will execute if `value` is 4 and `25.0 < powerPercentage` is true.
return "Increase speed";
}
"STOP" => {
// This block will execute if `value` is "STOP".
return "STOP";
}
switchStatus => {
// This block will execute if `value` is equal
// to the value of the `switchStatus` constant.
return "Switch ON";
}
// Destructuring a tuple with type checking
[var x, var y] if x is decimal && y is decimal => {
return string `Maneuvering to x: ${x.toString()} and y: ${y.toString()
} coordinates`;
}
// Destructuring a map and recursively matching with optional argument
{x: var a, y: var b, ...var rest} => {
string optionalArg = matchValue(rest, isObstructed, powerPercentage);
return string `Maneuvering to x: ${a.toString()} and y: ${b.toString()
} coordinates with ${optionalArg}`;
}
_ => {
// This block will execute for any other unmatched value.
return "Invalid instruction";
}
}
}
public function main() {
string output = matchValue([-2.516d, 51.409d], false, 0.0);
io:println(output);
}
import java.util.Map;
class Main {
static final String switchStatus = "ON";
public static String matchValue(Object value, boolean isObstructed,
float powerPercentage) {
switch (value) {
case Integer i -> {
if (i == 1 && !isObstructed) {
return "Move forward";
}
if (i == 2 || i == 3) {
return "Turn";
}
if (i == 4 && powerPercentage > 25.0) {
return "Increase speed";
}
return "Invalid instruction";
}
case String str -> {
if (str.equals("STOP")) {
return "STOP";
}
if (str.equals(switchStatus)) {
return "Switch ON";
}
return "Invalid instruction";
}
case double[] arr -> {
if (arr.length == 2) {
return "Maneuvering to x: " + arr[0]
+ " and y: " + arr[1] + " coordinates";
} else {
return "Invalid instruction";
}
}
case Record record -> {
double a = record.x;
double b = record.y;
Map<String, Object> rest = record.rest;
String optionalArg = matchValue(rest, isObstructed, powerPercentage);
return "Maneuvering to x: " + a + " and y: " + b +
" coordinates with " + optionalArg;
}
default -> {
return "Invalid instruction";
}
}
}
record Record(double x, double y, Map<String, Object> rest) {}
public static void main(String[] args) {
String output = matchValue(new double[] { 2.516, 51.409 }, false, 0.0f);
System.out.println(output);
}
}
import ballerina/io;
const switchStatus = "ON";
function matchValue(anydata value, boolean isObstructed,
float powerPercentage) returns string {
// The value of the `val` variable is matched against the given value match patterns.
match value {
1 if !isObstructed => {
// This block will execute if `value` is 1 and `isObstructed` is false.
return "Move forward";
}
// `|` is used to match more than one value.
2|3 => {
// This block will execute if `value` is either 2 or 3.
return "Turn";
}
4 if 25.0 < powerPercentage => {
// This block will execute if `value` is 4 and `25.0 < powerPercentage` is true.
return "Increase speed";
}
"STOP" => {
// This block will execute if `value` is "STOP".
return "STOP";
}
switchStatus => {
// This block will execute if `value` is equal
// to the value of the `switchStatus` constant.
return "Switch ON";
}
// Destructuring a tuple with type checking
[var x, var y] if x is decimal && y is decimal => {
return string `Maneuvering to x: ${x.toString()} and y: ${y.toString()
} coordinates`;
}
// Destructuring a map and recursively matching with optional argument
{x: var a, y: var b, ...var rest} => {
string optionalArg = matchValue(rest, isObstructed, powerPercentage);
return string `Maneuvering to x: ${a.toString()} and y: ${b.toString()
} coordinates with ${optionalArg}`;
}
_ => {
// This block will execute for any other unmatched value.
return "Invalid instruction";
}
}
}
public function main() {
string output = matchValue([-2.516d, 51.409d], false, 0.0);
io:println(output);
}
Data validation at the boundary
Boundary data validation is crucial for data-oriented programming. It ensures only valid and reliable data enters the system, improving data integrity, downstream processing, and security.
Ballerina, with its built-in language features, handles data validation automatically. In Java, libraries like Hibernate Validator and Apache Commons Validator provide tools for enforcing validation rules.
Java
import jakarta.validation.Valid;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collections;
import java.util.stream.Collectors;
@SpringBootApplication
@RestController
public class Main {
@PostMapping("/user")
public ResponseEntity<String> handleRequest(@Valid @RequestBody User user) {
return ResponseEntity.ok("User " + user.username() + " signed up successfully");
}
public static void main(String[] args) {
SpringApplication app = new SpringApplication(Main.class);
app.setDefaultProperties(Collections.singletonMap("server.port", "9090"));
app.run(args);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<String> handleException(MethodArgumentNotValidException ex) {
String errorMessage = ex.getBindingResult().getFieldErrors().stream()
.map(this::getViolationMessage)
.collect(Collectors.joining(", "));
return ResponseEntity.badRequest().body("Payload validation failed: " + errorMessage);
}
private String getViolationMessage(FieldError fieldError) {
String fieldName = fieldError.getField();
String constraintName = fieldError.getCode();
return String.format("Validation failed for '%s:%s'", fieldName, constraintName);
}
record User(
@Size(min = 1, max = 8,
message = "Username is not valid") String username,
@Pattern(regexp = "^[\\S]{4,}$",
message = "Password should be greater than 4") String password
) {}
}
Ballerina
import ballerina/constraint;
import ballerina/http;
import ballerina/io;
type User record {
@constraint:String {
minLength: 1,
maxLength: 8
}
string username;
@constraint:String {
pattern: re `^[\S]{4,}$`
}
string password;
};
service / on new http:Listener(9090) {
resource function post user(User user) returns http:Created {
io:println(string `User ${user.username} signed up successfully`);
return http:CREATED;
}
}
import jakarta.validation.Valid;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collections;
import java.util.stream.Collectors;
@SpringBootApplication
@RestController
public class Main {
@PostMapping("/user")
public ResponseEntity<String> handleRequest(@Valid @RequestBody User user) {
return ResponseEntity.ok("User " + user.username() + " signed up successfully");
}
public static void main(String[] args) {
SpringApplication app = new SpringApplication(Main.class);
app.setDefaultProperties(Collections.singletonMap("server.port", "9090"));
app.run(args);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<String> handleException(MethodArgumentNotValidException ex) {
String errorMessage = ex.getBindingResult().getFieldErrors().stream()
.map(this::getViolationMessage)
.collect(Collectors.joining(", "));
return ResponseEntity.badRequest().body("Payload validation failed: " + errorMessage);
}
private String getViolationMessage(FieldError fieldError) {
String fieldName = fieldError.getField();
String constraintName = fieldError.getCode();
return String.format("Validation failed for '%s:%s'", fieldName, constraintName);
}
record User(
@Size(min = 1, max = 8,
message = "Username is not valid") String username,
@Pattern(regexp = "^[\\S]{4,}$",
message = "Password should be greater than 4") String password
) {}
}
import ballerina/constraint;
import ballerina/http;
import ballerina/io;
type User record {
@constraint:String {
minLength: 1,
maxLength: 8
}
string username;
@constraint:String {
pattern: re `^[\S]{4,}$`
}
string password;
};
service / on new http:Listener(9090) {
resource function post user(User user) returns http:Created {
io:println(string `User ${user.username} signed up successfully`);
return http:CREATED;
}
}
Data immutability
Immutable data ensures data integrity, simplifies reasoning about code, and reduces the potential for unexpected side effects.
Ballerina and Java approach data immutability differently. In Ballerina, immutability is emphasized by default, providing deep immutability for data. On the other hand, in Java, a record
is considered to be shallowly immutable.
Java
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
class Main {
record Student(int grade, String name, Map<String, Integer> marks) {}
public static void main(String[] args) {
Map<String, Integer> marks = new HashMap<>();
marks.put("Maths", 75);
marks.put("English", 90);
Student student1 = new Student(12, "John", marks);
// student1.course.credits = 4; // Compile time error
// student1.marks = new HashMap<>(); // Compile time error
student1.marks.put("English", 95); // Shallow immutability
Student student2 = new Student(12, "John", Collections.unmodifiableMap(marks));
student2.marks.put("English", 95); // Fails at runtime
}
}
Ballerina
type Student record {|
int grade;
string name;
map<int> marks;
|};
public function main() {
Student & readonly student = {
grade: 12,
name: "John",
// The applicable contextually-expected type for marks now is `map<int> & readonly`.
// Thus, the value for marks will be constructed as an immutable map.
marks: {
"Maths": 75,
"English": 90
}
};
// student.grade = 11; // Compile time error
// student.marks["Maths"] = 80; // Compile time error
}
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
class Main {
record Student(int grade, String name, Map<String, Integer> marks) {}
public static void main(String[] args) {
Map<String, Integer> marks = new HashMap<>();
marks.put("Maths", 75);
marks.put("English", 90);
Student student1 = new Student(12, "John", marks);
// student1.course.credits = 4; // Compile time error
// student1.marks = new HashMap<>(); // Compile time error
student1.marks.put("English", 95); // Shallow immutability
Student student2 = new Student(12, "John", Collections.unmodifiableMap(marks));
student2.marks.put("English", 95); // Fails at runtime
}
}
type Student record {|
int grade;
string name;
map<int> marks;
|};
public function main() {
Student & readonly student = {
grade: 12,
name: "John",
// The applicable contextually-expected type for marks now is `map<int> & readonly`.
// Thus, the value for marks will be constructed as an immutable map.
marks: {
"Maths": 75,
"English": 90
}
};
// student.grade = 11; // Compile time error
// student.marks["Maths"] = 80; // Compile time error
}
XML support
XML is a structured markup language that offers a flexible and extensible approach for representing data.
Ballerina's XML native support enables seamless parsing, generation, and manipulation of XML data, facilitating integration with XML-based systems and protocols in data-oriented programming. Java developers can utilize third-party libraries to achieve similar XML handling capabilities.
Java
import org.modelmapper.ModelMapper;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import java.io.ByteArrayInputStream;
import java.util.Iterator;
import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.*;
class Main {
// Define a SOAP payload
final static String soapPayload =
"""
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Body>
<person>
<name>John Doe</name>
<age>30</age>
<address>
<city>New York</city>
<country>USA</country>
</address>
</person>
</soapenv:Body>
</soapenv:Envelope>""";
public record Address(String city, String country) {
public Address() {
this(null, null);
}
}
public static void main(String[] args) throws Exception {
// Parse the SOAP payload
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.parse(new ByteArrayInputStream(soapPayload.getBytes()));
// Create an XPath instance
XPath xpath = XPathFactory.newInstance().newXPath();
NamespaceContext nsContext = new NamespaceContext() {
public String getNamespaceURI(String prefix) {
if (prefix.equals("soapenv")) {
return "http://schemas.xmlsoap.org/soap/envelope/";
}
return null;
}
public String getPrefix(String namespaceURI) {
return null;
}
public Iterator<String> getPrefixes(String namespaceURI) {
return null;
}
};
xpath.setNamespaceContext(nsContext);
// Navigate to SOAP payload and extract the data using XPath
String soapPayloadExpression = "/*/soapenv:Body";
Node soapPayloadNode = (Node) xpath
.evaluate(soapPayloadExpression, document, XPathConstants.NODE);
String personPath = "./person";
Node personNode = (Node) xpath.evaluate(personPath, soapPayloadNode,
XPathConstants.NODE);
String name = (String) xpath.evaluate("name", personNode,
XPathConstants.STRING);
String age = (String) xpath.evaluate("age", personNode,
XPathConstants.STRING);
String city = (String) xpath.evaluate("*/city", personNode,
XPathConstants.STRING);
// Extract the sub-xml and convert it to a record
String addressPath = "./address";
Node addressNode = (Node) xpath.evaluate(addressPath, personNode,
XPathConstants.NODE);
Address address = new ModelMapper().map(addressNode, Address.class);
String country = address.country();
System.out.println("Name: " + name + ", Age: " + age
+ ", City: " + city + ", Country: " + country);
}
}
Ballerina
import ballerina/io;
import ballerina/xmldata;
// Define a SOAP payload
xml soapPayload =
xml `<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Body>
<person>
<name>John Doe</name>
<age>30</age>
<address>
<city>New York</city>
<country>USA</country>
</address>
</person>
</soapenv:Body>
</soapenv:Envelope>`;
xmlns "http://schemas.xmlsoap.org/soap/envelope/" as ns;
type address record {|
string city;
string country;
|};
public function main() returns error? {
// Extract the SOAP payload
xml xmlPayload = soapPayload/**/<ns:Body>;
io:println(xmlPayload);
// Navigate to the subcontext and extract the data
xml person = xmlPayload/<person>;
string name = (person/<name>).data();
string age = (person/<age>).data();
string city = (person/**/<city>).data();
// Extract the sub-xml and convert it to a record
address address = check xmldata:fromXml(person/<address>);
string country = address.country;
io:println(string `Name: ${name}, Age: ${age}, City: ${city}, Country: ${country}`);
}
import org.modelmapper.ModelMapper;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import java.io.ByteArrayInputStream;
import java.util.Iterator;
import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.*;
class Main {
// Define a SOAP payload
final static String soapPayload =
"""
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Body>
<person>
<name>John Doe</name>
<age>30</age>
<address>
<city>New York</city>
<country>USA</country>
</address>
</person>
</soapenv:Body>
</soapenv:Envelope>""";
public record Address(String city, String country) {
public Address() {
this(null, null);
}
}
public static void main(String[] args) throws Exception {
// Parse the SOAP payload
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.parse(new ByteArrayInputStream(soapPayload.getBytes()));
// Create an XPath instance
XPath xpath = XPathFactory.newInstance().newXPath();
NamespaceContext nsContext = new NamespaceContext() {
public String getNamespaceURI(String prefix) {
if (prefix.equals("soapenv")) {
return "http://schemas.xmlsoap.org/soap/envelope/";
}
return null;
}
public String getPrefix(String namespaceURI) {
return null;
}
public Iterator<String> getPrefixes(String namespaceURI) {
return null;
}
};
xpath.setNamespaceContext(nsContext);
// Navigate to SOAP payload and extract the data using XPath
String soapPayloadExpression = "/*/soapenv:Body";
Node soapPayloadNode = (Node) xpath
.evaluate(soapPayloadExpression, document, XPathConstants.NODE);
String personPath = "./person";
Node personNode = (Node) xpath.evaluate(personPath, soapPayloadNode,
XPathConstants.NODE);
String name = (String) xpath.evaluate("name", personNode,
XPathConstants.STRING);
String age = (String) xpath.evaluate("age", personNode,
XPathConstants.STRING);
String city = (String) xpath.evaluate("*/city", personNode,
XPathConstants.STRING);
// Extract the sub-xml and convert it to a record
String addressPath = "./address";
Node addressNode = (Node) xpath.evaluate(addressPath, personNode,
XPathConstants.NODE);
Address address = new ModelMapper().map(addressNode, Address.class);
String country = address.country();
System.out.println("Name: " + name + ", Age: " + age
+ ", City: " + city + ", Country: " + country);
}
}
import ballerina/io;
import ballerina/xmldata;
// Define a SOAP payload
xml soapPayload =
xml `<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Body>
<person>
<name>John Doe</name>
<age>30</age>
<address>
<city>New York</city>
<country>USA</country>
</address>
</person>
</soapenv:Body>
</soapenv:Envelope>`;
xmlns "http://schemas.xmlsoap.org/soap/envelope/" as ns;
type address record {|
string city;
string country;
|};
public function main() returns error? {
// Extract the SOAP payload
xml xmlPayload = soapPayload/**/<ns:Body>;
io:println(xmlPayload);
// Navigate to the subcontext and extract the data
xml person = xmlPayload/<person>;
string name = (person/<name>).data();
string age = (person/<age>).data();
string city = (person/**/<city>).data();
// Extract the sub-xml and convert it to a record
address address = check xmldata:fromXml(person/<address>);
string country = address.country;
io:println(string `Name: ${name}, Age: ${age}, City: ${city}, Country: ${country}`);
}
JSON support
Ballerina and Java both support JSON, a lightweight data-interchange format. Ballerina has native JSON support, allowing seamless integration with JSON-based systems and APIs. In Java, external libraries like Jackson or Gson provide comprehensive JSON processing capabilities.
Java
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
record InvoiceItem(String id, double price, boolean taxable) {}
record Customer(String id, String name) {}
record Invoice(String id, Customer customer, List<InvoiceItem> items) {}
class Main {
public static void main(String[] args) throws IOException {
String invoiceData = Files.readString(Paths.get("./invoice.json"));
// Parse the JSON string
Gson gson = new Gson();
JsonObject jsonObj = gson.fromJson(invoiceData, JsonObject.class);
// Fails at runtime if the key is not present or the value is not a string.
String id = jsonObj.get("id").getAsString();
System.out.println("Invoice id: " + id);
// Fails at runtime if the key is not present.
JsonArray items = jsonObj.getAsJsonArray("items");
System.out.println("Invoice items: " + items);
// Results in a null value if the accessed field is not present.
JsonObject secondItem = items.get(1).getAsJsonObject();
if (secondItem.has("discount")) {
double discount = secondItem.get("discount").getAsDouble();
System.out.println("Discount: " + discount);
}
// Converts to the domain type.
// Fails at runtime if the json value does not match the type.
Invoice invoice = gson.fromJson(invoiceData, Invoice.class);
// Access the fields of the domain type.
id = invoice.id();
List<InvoiceItem> invoiceItems = invoice.items();
System.out.println("Invoice items: " + invoiceItems);
}
}
Ballerina
import ballerina/io;
type InvoiceItem record {
string id;
decimal price;
boolean taxable;
};
type Customer record {
string id;
string name;
};
type Invoice record {
string id;
Customer customer;
InvoiceItem[] items;
};
public function main() returns error?{
json invoiceData = check io:fileReadJson("./invoice.json");
// Enjoy lax static typing here!
// Fails at runtime if the key is not present or the value is not a string.
string id = check invoiceData.id;
io:println("Invoice id: ", id);
// Fails at runtime if the key is not present.
json items = check invoiceData.items;
io:println("Invoice items: ", items);
// Fails at runtime if the convertion is not possible.
json[] itemArr = check items.cloneWithType();
// Results in a nil value if the accessed field is not present.
decimal? discountAmount = check itemArr[1]?.discount?.amount;
io:println("Discount amount: ", discountAmount);
// Converts to the domain type.
// Fails at runtime if the json value does not match the type.
Invoice invoice = check invoiceData.fromJsonWithType();
// Enjoy type-safe handling of json values.
id = invoice.id;
InvoiceItem[] invoiceItems = invoice.items;
io:println("Invoice items: ", invoiceItems);
}
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
record InvoiceItem(String id, double price, boolean taxable) {}
record Customer(String id, String name) {}
record Invoice(String id, Customer customer, List<InvoiceItem> items) {}
class Main {
public static void main(String[] args) throws IOException {
String invoiceData = Files.readString(Paths.get("./invoice.json"));
// Parse the JSON string
Gson gson = new Gson();
JsonObject jsonObj = gson.fromJson(invoiceData, JsonObject.class);
// Fails at runtime if the key is not present or the value is not a string.
String id = jsonObj.get("id").getAsString();
System.out.println("Invoice id: " + id);
// Fails at runtime if the key is not present.
JsonArray items = jsonObj.getAsJsonArray("items");
System.out.println("Invoice items: " + items);
// Results in a null value if the accessed field is not present.
JsonObject secondItem = items.get(1).getAsJsonObject();
if (secondItem.has("discount")) {
double discount = secondItem.get("discount").getAsDouble();
System.out.println("Discount: " + discount);
}
// Converts to the domain type.
// Fails at runtime if the json value does not match the type.
Invoice invoice = gson.fromJson(invoiceData, Invoice.class);
// Access the fields of the domain type.
id = invoice.id();
List<InvoiceItem> invoiceItems = invoice.items();
System.out.println("Invoice items: " + invoiceItems);
}
}
import ballerina/io;
type InvoiceItem record {
string id;
decimal price;
boolean taxable;
};
type Customer record {
string id;
string name;
};
type Invoice record {
string id;
Customer customer;
InvoiceItem[] items;
};
public function main() returns error?{
json invoiceData = check io:fileReadJson("./invoice.json");
// Enjoy lax static typing here!
// Fails at runtime if the key is not present or the value is not a string.
string id = check invoiceData.id;
io:println("Invoice id: ", id);
// Fails at runtime if the key is not present.
json items = check invoiceData.items;
io:println("Invoice items: ", items);
// Fails at runtime if the convertion is not possible.
json[] itemArr = check items.cloneWithType();
// Results in a nil value if the accessed field is not present.
decimal? discountAmount = check itemArr[1]?.discount?.amount;
io:println("Discount amount: ", discountAmount);
// Converts to the domain type.
// Fails at runtime if the json value does not match the type.
Invoice invoice = check invoiceData.fromJsonWithType();
// Enjoy type-safe handling of json values.
id = invoice.id;
InvoiceItem[] invoiceItems = invoice.items;
io:println("Invoice items: ", invoiceItems);
}
Model data streams
In data-oriented programming, efficient handling and processing of large amounts of data is vital. Ballerina's built-in stream type enables developers to process data on-demand, apply transformations, filters, and aggregations, and facilitates seamless integration with other data processing operations.
import ballerina/io;
type SensorData record {|
string sensorName;
string timestamp;
float temperature;
float humidity;
|};
public function main(string filePath = "sensor_data.csv") returns error? {
// Read file as a stream which will be lazily evaluated
stream<SensorData, error?> sensorDataStrm = check io:fileReadCsvAsStream(filePath);
map<float> ecoSenseAvg = check map from var {sensorName, temperature} in sensorDataStrm
// if sensor reading is faulty; stops processing the file
let float tempInCelcius = check convertTemperatureToCelcius(sensorName, temperature)
group by sensorName
select [sensorName, avg(tempInCelcius)];
io:println(ecoSenseAvg);
}
function convertTemperatureToCelcius(string sensorName, float temperature) returns float|error {
if temperature < 0.0 || temperature > 10000.0 {
return error(string `Invalid kelvin temperature value in sensor: ${sensorName}`);
}
return temperature - 273.15;
}
Model tabular data
Tabular data modeling empowers developers to effectively organize, process, and manipulate structured data, leading to more modular, maintainable, and efficient data-oriented programs.
Ballerina, with its built-in table
data type, provides native support for modeling and manipulating tabular data, allowing you to define records as values and associate them with unique keys.
import ballerina/io;
// Define a type for tabular data
type Employee record {|
readonly int id;
string name;
readonly string department;
int salary;
|};
// Create an in-memory table with compound keys
table<Employee> key(id, department) employeeTable = table [
{id: 1, name: "John Doe", department: "Engineering", salary: 5000},
{id: 2, name: "Jane Smith", department: "Sales", salary: 4000}
];
public function main() {
// Add an employee to the table
employeeTable.add({id: 3, name: "William Smith", department: "Engineering", salary: 4500});
// Adding duplicate record, throws KeyAlreadyExist error
// employeeTable.add({id: 2, name: "Jane Smith", department: "Sales", salary: 5000});
// Putting duplicate record, overrides the existing value
employeeTable.put({id: 2, name: "Jane Smith", department: "Sales", salary: 5000});
// Retrieve an employee using the compound key
Employee? employee = employeeTable[1, "Engineering"];
if (employee is Employee) {
io:println("Employee Found: " + employee.name);
} else {
io:println("Employee Not Found");
}
// Calculate the total salary in the Engineering department
int totalSalary = from var {department, salary} in employeeTable
where department == "Engineering"
collect int:sum(salary);
io:println(string `Total Salary in Engineering Department: ${totalSalary}`);
}