Data-Oriented Programming in Rust
Introduction: A Relatable Struggle
I remember redesigning a particular application in object-oriented programming with UML for hours. Every time, I would tweak it for “better abstractions,” layering abstractions on top of abstractions to make it as “flexible as possible” for future requirements. Each iteration made the application harder to reason about, and when threading entered the mix, things got even worse.
This experience made me deeply skeptical of unnecessary complexity in software design. While object-oriented programming (OOP) works well for some problems, it often adds unnecessary layers of abstraction that turn into liabilities over time. Nowadays, I lean more toward functional and data-driven approaches. My recent applications use very few abstractions - inheritance is almost nonexistent, and I rely heavily on interfaces and composition.
I really love this talk if you want to dig deeper on paradigms: The Forgotten Art of Structured Programming - Kevlin Henney, 2019
The Appeal of Data-Oriented Programming
Recently, I was tinkering with Data-Oriented Programming by Yehonathan Sharvit. The book mostly uses JavaScript and Java to explain well-known problems in OOP. The first chapter caught my attention: it proposes splitting the design into data classes and method classes.
I was trying to grasp the author’s message, and the core idea of the book became clear: when you have a complex UML diagram, it’s often cluttered with arrows - hierarchies between classes and interactions among them - that result in overly complicated designs. One major issue in object-oriented code is the mixing of data and behavior within the same structures.
What if, instead, we split the diagram into data classes and method classes? Suddenly, the design becomes much simpler. Most inheritance and composition arrows are confined to the data classes, while the action arrows are isolated within the method classes. This separation creates a system that’s easier to reason about and model. It drastically reduces design complexity - by an order of magnitude - and makes adding features far more straightforward. No more deadly diamonds of death, and the entire structure becomes more maintainable.
In OOP, mixing data and behavior into tightly coupled objects creates complex hierarchies. Data becomes scattered across the hierarchy, shared between objects, and often requires synchronization in threaded applications. By isolating data into immutable structures (following chapters), the book argues, you can maintain a single source of truth for the application’s state. This approach makes it inherently safer for multi-threaded code.
The idea is simple but powerful: by separating data from behavior, you simplify the design, reduce unnecessary inheritance, and make systems easier to reason about and more threads friendly.
The book’s insights make sense for JavaScript, where mutable data is hard to control. Immutability can solve many problems, even if it sometimes means copying data unnecessarily - e.g., filtering a large JSON object just to extract a few fields for use as method parameters, a scenario that occurs frequently. Thankfully, JavaScript engines are optimized for this kind of workload.
Fun fact: In my most recent JavaScript project, I naturally gravitated toward solutions similar to those proposed in the book, even without consciously applying Data-Oriented Programming principles. With this new perspective, I plan to revisit my architecture through the lens of DOP to see if there’s anything I might have overlooked.
Rust’s Unique Approach
But as I thought about applying these ideas in Rust, I realized something: Rust takes a different path to solve the same problems. Instead of relying on immutability, Rust enforces ownership and borrowing rules that eliminate shared mutable state at compile time. This achieves thread safety and simpler designs without the performance trade-offs of immutability.
Here’s how Rust handles data:
- Ownership: A function can take ownership of data, meaning it’s the only one allowed to use it:
fn fun(data: DataType);
- Immutable References: A function can borrow an immutable reference, allowing multiple readers but no mutation:
fn fun(&data: DataType);
- Mutable References: A function can borrow a mutable reference, allowing exclusive mutation but no sharing:
fn fun(&mut data: DataType);
And when it comes to threads, Rust provides powerful abstractions to ensure memory safety:
- Channels: Transfer ownership of data between threads safely.
- Locks: Rust enforces that locked data can only be accessed while the lock is held, preventing accidental sharing.
- Send and Sync Traits: Rust statically ensures that types are only shared across threads when it’s safe to do so.
By design, Rust makes it impossible to accidentally introduce shared mutable state, which is often the root cause of concurrency bugs.
Aligning Rust with Data-Oriented Programming
Rust’s approach is inherently data-oriented. The focus on ownership means data structures are self-contained, and the type system encourages linear, cache-friendly layouts like Vec
or slices. Unlike the class hierarchies of OOP, Rust’s data structures are simple and efficient by default, aligning with the principles of data-oriented programming.
That said, the DOP approach described in Sharvit’s book still appeals to me. For JavaScript and TypeScript projects, separating data and methods can significantly simplify designs, especially for web applications where immutability works well with reactive frameworks.
Conclusion: Choosing the Right Tool
In the end, while Rust and DOP take different paths, they share the same goal: reducing complexity and making applications easier to think about.
For JavaScript and TypeScript projects, I’ll happily apply DOP principles in my next designs. For Rust, I’m excited to explore how its ownership model and other paradigms can influence data-heavy systems.
What about you? Are you embracing these approaches in your own projects? Let me know how they’ve worked for you - or what challenges you’ve faced - when simplifying your designs.
Enjoy Reading This Article?
Here are some more articles you might like to read next: