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.

Download Ballerina

Data-oriented programming

Model data as data

Data-oriented programming promotes the idea of representing data in its purest form. Ballerina's records simplify this approach, enabling concise and effective data representation.

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

In data-oriented programming, modeling choices is essential for achieving code-data separation. This approach results in modular, maintainable, and extensible code that can handle different data variants in a unified and type-safe manner. Ballerina offers built-in mechanisms to model choices as discriminate unions, using a concise and seamlessly integrated syntax.

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 takes precedence, modeling optionality is crucial for expressing the presence or absence of data in a concise and type-safe manner.

Optional typing enables the indication of nullable or absent values, while optional fields offer flexibility in representing different data states.

Ballerina has built-in support for optional types and fields, eliminating the risk of null pointer exceptions and related bugs, ensuring more robust code.

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.

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 is a powerful tool in data-oriented programming that allows developers to extract relevant data from complex patterns and perform specific operations based on the structure and content of the data.

Ballerina provides concise and expressive pattern matching techniques to handle intricate data structures efficiently.

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 a critical aspect of data-oriented programming as it ensures that only valid and reliable data is allowed into the system enhancing data integrity, downstream processing, and security.

Ballerina, with its built-in language features, handles the validation process automatically, ensuring that only valid data is accepted.

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 is a key aspect that ensures data integrity, simplifies code reasoning, and minimizes the risk of unexpected side effects.

In Ballerina, immutability is emphasized by default, offering deep immutability for data. This approach promotes a safer programming environment, where data remains unchanged once created, enhancing code reliability and predictability.

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, as a structured markup language, provides a flexible and extensible means of representing data. Ballerina's native support for XML enables smooth parsing, generation, and manipulation of XML data. This support greatly facilitates integration with XML-based systems and protocols within data-oriented programming, enhancing interoperability and data exchange capabilities.

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 offers native support for JSON, enabling effortless integration with JSON-based systems and APIs. This support ensures smooth handling and manipulation of JSON data within data-oriented programming, enhancing interoperability and facilitating seamless communication with JSON-based systems.

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}`);
}