Data-oriented programming

Java’s Paradigm Shift: Embracing Data-Oriented Programming

For decades, Object-Oriented Programming (OOP) has reigned supreme in Java, modeling systems as a network of objects that bundle state and behavior. This paradigm is excellent for managing complex, evolving processes. However, as modern applications increasingly focus on data transformation and concurrency, a different approach has risen to prominence: Data-Oriented Programming (DOP). Java has embraced this shift, providing powerful, native tools to support it.

 

What is DOP? A New Focus on Data

Data-Oriented Programming (DOP) prioritizes data as a fundamental element, advocating for a clear distinction between code (logic) and data (structure). In contrast to Object-Oriented Programming (OOP), where objects manage their internal state, DOP promotes the use of simple, immutable data processed by pure functions. This paradigm adheres to three core principles: data should be immutable, transparent, and structured to prevent invalid states.

 

DOP vs OOP: The Core Trade-off

The distinction between DOP and OOP highlights a fundamental trade-off in software design, often referred to as the Expression Problem.

Feature Object-Oriented Programming (OOP) Data-Oriented Programming (DOP)
Focus Objects (State + Behavior) Data (Structure and Flow)
Data State Typically Mutable (managed internally) Primarily Immutable (transparent)
Extensibility Easier to add new data types — behavior lives with the data. Easier to add new behaviors — functions live apart from the data.

 

Why Adopt DOP? Benefits for Modern Java

Adopting a DOP style immediately yields several practical advantages. First, immutability is the cornerstone of robust concurrency, eliminating the vast majority of thread safety concerns related to shared mutable state. Second, separating logic into pure functions that operate on simple data dramatically simplifies unit testing, as functions are deterministic and side-effect-free. Finally, concise, transparent data models drastically reduce boilerplate and cognitive load, leading to more maintainable codebases.

 

Choosing the Right Tool: When to Use DOP

OOP remains the natural choice for high-level system boundaries, managing complex I/O, and defining services. However, DOP is ideally suited for tasks where data integrity and transformation are key: defining Value Objects, modeling Data Transfer Objects (DTOs), and especially for modeling fixed-choice domains using Algebraic Data Types (ADTs).

 

DOP in Java: The Modern Toolkit

Java’s journey toward embracing DOP has been a deliberate evolution, primarily delivered through Project Amber. This shift is not about replacing OOP but about augmenting it with powerful, idiomatic tools.

 

Java’s Evolution: The ADT Triplet

Modern Java leverages a powerful combination of features to support Data-Oriented Programming (DOP). These features collectively form Algebraic Data Types (ADTs), which are the cornerstone of data-oriented design.

Key components include:

  1. Records (JDK 16): These serve as the data carriers or Product Types, offering a concise, immutable, and final way to model data aggregates. They significantly reduce boilerplate by automatically generating essential methods like constructors, accessors, equals(), and hashCode(). Making them comparable by its data only.
  2. Sealed Classes/Interfaces (JDK 17): Providing the necessary constraints, these define a limited and closed set of permissible subclasses. When used in conjunction with Records, they form Sum Types, which are data structures that can only represent one of a finite number of predefined types.
  3. Pattern Matching for switch (JDK 21): This feature introduces the logic and behavior needed to safely and succinctly deconstruct data based on its specific type. A crucial benefit is that the compiler guarantees all possible types within a sealed hierarchy are handled, effectively shifting potential runtime errors to compile time.

 

Code Comparison: Difference between approaches

 

Object-oriented approach

 

abstract class Event {
    private final long timestamp;

    public Event() {
        this.timestamp = System.currentTimeMillis();
    }

    public long getTimestamp() {
        return timestamp;
    }

    public abstract void process(EventBusProcessor processor);
}

class UserCreatedEvent extends Event {
    private final String username;
    private final String email;

    public UserCreatedEvent(String username, String email) {
        this.username = username;
        this.email = email;
    }

    public String getUsername() { return username; }
    public String getEmail() { return email; }

    @Override
    public void process(EventBusProcessor processor) {
        processor.handle(this);
    }
}

class OrderPlacedEvent extends Event {
    private final int orderId;
    private final double amount;

    public OrderPlacedEvent(int orderId, double amount) {
        this.orderId = orderId;
        this.amount = amount;
    }

    public int getOrderId() { return orderId; }
    public double getAmount() { return amount; }

    @Override
    public void process(EventBusProcessor processor) {
        processor.handle(this);
    }
}

class EventBusProcessor {

    public void processEvent(Event event) {
        event.process(this); 
    }

    public void handle(UserCreatedEvent event) {
        System.out.println("Processing UserCreatedEvent (Timestamp: " + event.getTimestamp() + ")");
        System.out.println("-> Logging: New user '" + event.getUsername() + "' with email " + event.getEmail());
    }

    public void handle(OrderPlacedEvent event) {
        System.out.println("Processing OrderPlacedEvent (Timestamp: " + event.getTimestamp() + ")");
        System.out.println("-> Billing: Charge $" + event.getAmount() + " for order ID " + event.getOrderId());
    }
}

 

Data-oriented approach

 

sealed interface ApplicationEvent {
    long timestamp();
}

record UserCreatedEvent(String username, String email, long timestamp) implements ApplicationEvent {
    public UserCreatedEvent(String username, String email) {
        this(username, email, System.currentTimeMillis());
    }
}

record OrderPlacedEvent(int orderId, double amount, long timestamp) implements ApplicationEvent {
    public OrderPlacedEvent(int orderId, double amount) {
        this(orderId, amount, System.currentTimeMillis());
    }
}

class EventBusProcessor {
    public void processEvent(ApplicationEvent event) {
        switch (event) {
            case UserCreatedEvent(String username, String email, long timestamp) -> {
                System.out.println("Processing UserCreatedEvent (Timestamp: " + timestamp + ")");
                System.out.println("-> Logging: New user '" + username + "' with email " + email);
            }
            case OrderPlacedEvent(int orderId, double amount, long timestamp) -> {
                System.out.println("Processing OrderPlacedEvent (Timestamp: " + timestamp + ")");
                System.out.println("-> Billing: Charge $" + amount + " for order ID " + orderId);
            }
        }
    }
}

 

Object-Oriented Programming (OOP) relies on encapsulation, hierarchy and polimorphism for defining data and implementing logic. In contrast, the Data-Oriented Programming (DOP) approach distinctly separates the data model from the processing logic. This separation is enhanced by sealed classes and pattern matching for switch expressions.

 

Beyond Today: The Future of Data in Java

While modern Java has made huge strides toward DOP, the language still contends with fundamental limitations rooted in its original object model.

This is where Project Valhalla offers the radical, long-term solution. Valhalla aims to introduce Value Objects to the language, changing how data is stored in memory. This structural change will allow Records to be flattened in memory, achieving genuine data locality and dramatically closing the performance gap for high-throughput data processing tasks. Java is clearly moving toward a powerful hybrid model where compiler ergonomics meet deep runtime optimization.

 

Conclusion

Modern Java features like Records and Sealed Classes have delivered a powerful, idiomatic toolkit for embracing Data-Oriented Programming. This pragmatic evolution allows developers to choose the optimal paradigm: using OOP for defining system boundaries and DOP for building simple, immutable, and highly thread-safe data pipelines. By making code more concise and correct compiler-enforced, and with Project Valhalla promising deeper runtime performance gains, Java developers now have the complete toolkit to write efficient, future-proof software.

Personally, it makes me rethink how I use to make software. I’ve used it only for building data pipelines but I am open to new scenarios to apply it or mixing it with OOP in patterns like Functional Core, Imperative Shell.

Here you have a couple of links for further reading:

 

Author:

Guillermo Granado is a Senior Fullstack Software Engineer at Zartis, with over 10 years of experience delivering scalable and robust applications. He specializes in backend development, working extensively with Java and TypeScript, while also bringing solid experience in Python with experience provisioning infrastructure using Terraform in CI/CD workflows. He also has strong experience on the frontend side, particularly with modern frameworks like React and Angular, enabling him to contribute effectively across the entire development stack.

If you have more questions on the topic or require additional information, feel free to start a conversation and discover how we can help with your Java software development needs!

Share this post

Do you have any questions?

Zartis Tech Review

Your monthly source for AI and software news

;