If you read my last post, you already know why array-of-structures beats structure-of-arrays for cache performance. But DOD isn't just about memory layout — it's a mindset shift. And the pattern that embodies that shift most completely in Rust is ECS: Entity Component System.
The Problem with Inheritance
Remember when OOP promised code reuse through inheritance? You had GameObject, then Player extends GameObject, then Enemy extends GameObject, and then you needed a FlyingEnemy that inherits from both Player and Enemy and the whole thing collapsed into the diamond problem.
Rust doesn't give you classes. You knew that. But what you might not realize is: that's a feature, not a limitation.
Here's what happens when you try to model a game with inheritance in Rust:
// What you WANT to write (but can't)
class Enemy extends Character, Renderable, PhysicsBody {
// ...
}
Here's what ECS gives you instead:
use specs::{Entity, World, Builder};
// Define components — data, not behavior
#[derive(Component)]
struct Position { x: f32, y: f32 }
#[derive(Component)]
struct Velocity { dx: f32, dy: f32 }
#[derive(Component)]
struct Renderable { sprite: String }
#[derive(Component)]
struct Health { current: i32, max: i32 }
// Systems — behavior, not data
fn movement_system(pos: &mut Position, vel: &Velocity) {
pos.x += vel.dx;
pos.y += vel.dy;
}
Components are data. Systems are behavior. Entities are just IDs that connect components together. That's it. That's the whole pattern.
Why This Beats Inheritance
1. Composition over Inheritance
An entity isn't a type — it's a collection. A Player is just an entity with Position, Velocity, Renderable, and Health. An Enemy is an entity with the same components, maybe plus AIState. Want to make a FlyingEnemy? Add a Flying component. Done.
No inheritance hierarchy. No diamond problems. Just components.
2. Cache-Friendly by Design
Remember the AoS vs SoA from the DOD post? ECS libraries like specs and bevy_ecs store components in contiguous arrays. When you iterate over all Position components, you're touching memory sequentially. One cache line per iteration, not random pointer chasing through object graphs.
3. Runtime Flexibility
Inheritance is compile-time. ECS is runtime. Want to add a jetpack to a character mid-game? Just add the Flying component. The movement system will pick it up automatically because it queries for Position + Velocity, not for a specific type.
4. Parallelism for Free
Systems don't have mutable access to each other's components. A render_system reads Position and Renderable. A physics_system reads and writes Position and Velocity. The borrow checker is happy, and so is your CPU's multiple cores.
The Rust ECS Landscape
| Crate | Philosophy | Best For | |-------|-----------|----------| | Bevy ECS | Full engine, batteries included | Games, real-time apps | | specs | Pure ECS, no engine | Libraries, custom engines | | hecs | Minimal, fast | Embedded, WASM | | legion | Parallel-first | High-performance needs |
Bevy is the big one — it's a full game engine, and its ECS is famous for being ergonomic and fast. The tradeoff: it can feel like you're writing database queries, especially for simple problems. (This is why the Ply author called Bevy "virtual database queries" — it's a fair critique for non-game apps.)
For something lighter, hecs is 1,000 lines and blazing fast. Perfect for when you want ECS without the framework weight.
When to Use ECS
Here's the honest take: ECS is overkill for most applications. A CRUD web server? Use Actix or Axum. A CLI tool? Don't even think about it.
But for:
- Games with many entities (particles, enemies, projectiles)
- Simulation systems (physics, AI, rendering)
- Systems that need runtime-reconfigurable behavior
- Anything where cache locality matters at scale
ECS is the pattern that DOD practitioners reach for. It's what happens when you take the "data first, behavior second" philosophy and build an entire architecture around it.
The Bigger Picture
ECS isn't just a game dev pattern. It's a rejection of the idea that types should define behavior. In Rust, where traits and composition already fight against OOP thinking, ECS feels natural. You don't fight the borrow checker because ECS was designed to work with it.
The next time someone says "Rust doesn't have classes, how do I structure my code?", you have an answer: don't. Compose. Iterate. Let the data guide the design.
This post is part of a thread on Data-Oriented Design in Rust. Previous: Data-Oriented Design: The Secret to Fast Rust Code. Next: SIMD and you.