From OOP to Rust – struggling with code organization and data structure design

I'm a developer coming from a strong OOP background (Java/C#). In OOP, I had a clear mental model: everything is an object, and I could split code into classes without much doubt.

Now I'm learning Rust, and I feel genuinely lost. I'm questioning whether I'm actually a good developer, because I don't have clear criteria for:

  1. When to use a struct vs. just passing data around?
  2. How granular should my structs be? I'm afraid I'm either splitting too much or not enough.
  3. What is the "right" way to organize code in Rust? In OOP, I'd instinctively create small, cohesive classes. But in Rust, the same instinct feels arbitrary.

I know Rust isn't an OOP language, but what is its dominant paradigm? Is it "data-oriented", "functional", or something else? I want to understand the philosophy so I can think in Rust, not just translate OOP patterns.

I'm not looking for syntax help – I've read the Book. I need design principles , heuristics , or resources (books, articles) that teach the art of structuring Rust code correctly.

If you've been through this transition, what helped you the most? Are there any telltale signs that I'm over-engineering or under-engineering my data structures?

Thank you.

We had a long thread about OOP recently, see Basic Object Oriented Programming in Rust

I think, one reason why OOP was teached 30 years ago was because it was quite easy to teach and understand, and made it easy to structure code and allows reuse, at least in theory. But in real life programming OOP design was not always that great and straight forward. Personally I agree, that good design is Rust is a bit more difficult.

Don't worry, that feeling will go away. It's just the hangover.

A struct is just data with a name. You should use it when you need to group some data and refer to it by its name :slight_smile:.

That's domain-specific. Only you should know what are you modelling.

I would say it's 99% the same. The other 1% comes from the fact that Rust is a bit more "functional", and it has a proper sum type in the form of enums.

I would say Rust is pragmatic-oriented. It tends to use what is best on a case-by-case basis. But if you want to think in those terms, it's definitely more data-oriented, functional and imperative than probably most of the languages that you're used to.

You'll gain those insights either by practicing a lot or by reading already-existing code (crates). You could also hang around here on the forum; I'm quite sure you'll learn a lot from the more experienced Rust users!

In my case reading books, like Luca Palmieri's "Zero to Production in Rust", watching talks and reading articles about idiomatic Rust, and hanging around here on the forums.

You didn't ask about storing references in structs, so perhaps you already know this is problematic?

Despite some popular anti-OOP takes, there is a lot of overlap between basic OOP and good Rust design. By “basic” I mean concepts more like encapsulation — private fields and methods used as an interface to perform only valid operations on those fields — and less like high-level application architecture. Don’t feel you need to throw these concepts out.

  • When the data is inconvenient to pass around separately because it has too many parts.
  • When there is an invariant that should be maintained between parts of the data, and encapsulation can maintain that invariant.
  • When the data’s original type is too broad and a struct can give it useful type-safety (“newtype pattern”): struct RecordId(u64); and such.

Getting this truly right is a very tricky subject, because you're balancing three considerations where most languages have only two:

  1. Useful subdivisions of the program’s data that help the code be understandable
  2. Encapsulation boundaries around invariants
  3. Avoiding overly-broad borrows (unique to Rust)

There are no simple universal rules that can be clearly stated, here. Write what seems good, see how well it works, and be prepared to refactor. Never assume you can get the design right up front.

One Rust-specific thing to keep in mind is that it is often useful to have more than one data type where you might initially think you only need one. This is often the case when you have enums involved in a way that you might otherwise write as an inheritance hierarchy:

pub struct Shape {
    pub origin: Point,
    pub kind: ShapeKind,
}
pub enum ShapeKind {
    Circle { radius: f64 },
    Polygon(Vec<Point>),
    Text(String),
}

Common beginner mistakes are to either define separate structs for Circle, Polygon, and Text, or to use an enum with an origin field in every variant. Both of those will result in more code duplication than the above design.

Another case where two structs are often useful is when sharing something mutable; you’ll often need an Arc and a Mutex, and when that’s true, a useful encapsulation pattern is to put the Mutex and maybe the Arc inside your struct rather than letting it be accessed from outside, so that all access goes through your public methods.

#[derive(Clone)]
pub struct Database(Arc<Mutex<Inner>>);

struct Inner {
    records: HashMap<Id, Record>,
}

There are many more uses for defining multiple types that make up a single abstraction.

The right way to organize a program depends on where the complexity lies in that program; there is no single general principle. (There isn’t in OOP either; a lot of “the bad kind of OOP” is principles with fancy names getting over-enthusiastically applied when they don’t make the actual problem at hand better.)

When your organization is by topic, you do that with modules instead of classes. I mentioned above that there are often reasons to create multiple closely related types — those types go in modules.

This resonates deeply with me. Early on, I spent a lot of up-front effort agonizing over the types for my (imagined/inaccurate) full solution. Often, it's easier to group things as you go, however you see fit when building up functionality feature by feature: passing 3 things all together, bundle in struct; need disjoint borrows, split struct.

I forget where I read it, probably in Quote of the Week, but a good realization is that Rust's type system (local reasoning, stellar error messages) means that refactoring (even by hand without an IDE) basically becomes "paint by numbers" as the compiler guides you to all use sites. You kinda feel whether the refactor is helpful (simplifying usages) or neutral (rephrased field access) as you step through the compile errors you created.

I'll highlight one specific way this manifests which you may trip over at first: code which has access to private fields usually accesses those private fields directly, in contrast with using getters and setters. The reason being that getters/setters generally need to borrow the entire struct. Here's a related blog post.

(Getters/setters still exist for those outside the privacy boundary. Rust may eventually gain more fine-grained borrowing APIs ("view types"), but if it does, that will happen "some day" that isn't tomorrow.)

To my simple mind we can think of "struct" as "class". Heck in C++ struct and class are basically the same thing barring details about private fields.

As such there should be no more difficulty in dreaming up how you put your data into structs the there was deciding on classes.

At the simplest level if one has a bunch of data items that are used to implement something and then you want multiple instances of that thing that is a good candidate for a struct. Attaching the code that works on that data as methods.

Same idea as if one has a sequence of expressions that does something and then realise you need to do that thing in many places in the code, then you have a good candidate for a function.

I don't think we can see Rust as having a dominant paradigm. If you want be data-oriented or functional or whatever that should arise from the nature of the problem you are trying to solve not something you impose ahead of time on your design.

Struct being a class is close enough, but:

  • composition over inheritance is not merely preferred, but required (there's no inheritance. Rust's features that look like inheritance may not do what you'd expect from real OOP).

  • there's no "is a" modeling (Rectangle is a Shape). You either just write concrete types without hierarchy (just make struct Rectangle if all you need is width/height), or use traits approaching the problem not from what a type is, but what operation you want to perform and then select which types add support for that operation (impl Draw for Rectangle, or impl Draw for T where T: AsLines). If OOP was like organizing functionality by folders, traits are like organising functionality with a database where you select types by query.

  • the right size for a struct may be smaller than a class. This is because Rust is very strict about &mut being exclusive access (like a compile-time write lock), so &mut self on methods "locks" access to all fields of the struct at once for the duration of the method. You can borrow two different structs exclusively at the same time, but you can't borrow one struct instance exclusively twice, so sometimes it's useful to split a multi-purpose struct/class into more focused ones.

  • in Rust it's common to have structs that are just simple data, and don't use getters/setters. Rust won't let you hide borrowed vs owned type distinction behind an abstraction (temporarily borrowed types are limited in ways that can't be ignored). So typically you don't start with an abstraction and encapsulation defensively just in case. You start with concrete types and add abstractions where you have to.