Best practices for modeling systems that evolve over time — mutability, patterns, and performance?

Hi everyone — I’m new to Julia. I’ve known about the language for years but only recently started using it seriously.

I’m prototyping a space systems modeling tool — something that propagates orbits and performs optimization/estimation over complex physical models. Performance matters a lot here, and traditionally, systems like this have been written in Fortran, C, or C++ for that reason.

The core of the system involves evaluating flat physics models. But the interface is structured around structs to represent things like spacecraft, force models, etc. Most of these structs need to model change over time. For example, position, velocity, and mass may change during propagation or optimization.

So here’s the design tension:
Mutable structs are idiomatic in Julia, but I’m building a system that fundamentally models change. If I use immutable structs, I need to create new objects on every update, which breaks references used elsewhere to manage system state — and that gets messy fast. On the other hand, the actual mutation events (e.g., updating the struct after propagation) happen far less frequently than the inner calls to the ODE/physics models. So I suspect the cost of mutating the structs is negligible compared to the rest of the computation.

Right now, I’m using mutable structs and exposing the data using something like ComponentArrays (I rolled my own for now, but plan to migrate). This gives me a clean way to extract state into flat vectors for solvers, then write results back after each iteration or propagation.

So here’s my question:
What are the best practices in Julia for building complex, time-evolving systems like this?
Are there established design patterns or approaches for managing change cleanly in struct-based systems? Is mutability the right answer here, or is there a more Julian way I’m missing?

This seems like a well-trodden problem space, but I haven’t found a clear, idiomatic answer yet. I’d love to hear how others have tackled this — especially in simulation, modeling, or control system applications.

Thanks for any help or insights here!

Some simple sample structs, most of the fields change during propagation, optimization, or estimation.

mutable struct Spacecraft <: AbstractPoint
_state::AbstractState
_time::AbstractTime
_mass::AbstractMassModel
_tanks::AstractPropulsionModel
… this makes the point
end

mutable struct CelestialBody <: AbstractPoint
_name::String # i.e. Earth, Venus
_mu::Float64 # Gravitational parameter (km^3/s^2)
_equatorial_radius::Float64 # Equatorial radius (km)
_flattening::Float64 # Flattening (unitless)
end

Hey, is this supposed to be a discrete continuous system? If so and you’re mutating non-continuous values, then using a ComponentArray for u the continuous values and using a mutable struct for p, with DiscreteCallback/ContinuousCallback in the ODE solver, is a nice way to do it by hand. Or using ModelingToolkit the system can be built symbolically and then codegen to some form that is optimally flat with symbolic interfaces on top.

When it gets to modeling and you are in doubt you should follow @ChrisRackauckas advice. :grinning_face:

That being said, if you want to stick to your struct-based design or are interested in a general discussion, here are my thoughts about your example:

  • Often the “outer” structs need to be mutable for good performance, while the smaller “inner” structs can be immutable. In your case you probably could make CelestrialBody immutable and work with Ref if managing the updates is too complicated otherwise. The question is, whether it’s worth it in this case, because CelestrialBody seems to have object identity, so modeling it as a mutable struct seems to be the right thing.
  • Try to avoid abstract types in a struct definition. Your alternatives are
    • Concrete types (obviously)
    • Parameterized types
    • Union(All) types
  • Starting all field names with an underscore is not a typical Julia style. If you want to make sure that they are not accessed outside the module, a private-symbol check in Aqua.jl will probably be more robust (not implemented yet, however)
  • AbstractPoint might be misleading, as I would have assumed, that an AbstractPoint always has a position, which your objects do not seem to have. Maybe you find a better name (depending on your specific needs, maybe AbstractPointObject)
1 Like

Sorry for the interruption. I’m curious about why avoiding Abstract Types? This could be useful in context of AD? It there a severe issue in dynamic dispatching?

They bring a large performance penalty. Structs are fast if the can be stored as a continues piece of bytes. So concrete types are the fastest. If you need small (up to 100 element) vectors in your struct, use SVectors or MVectors, because they have a size that is known at compile time.

Example for using a union instead of an abstract type which helped a lot to reduce memory allocations: VortexStepMethod.jl/src/body_aerodynamics.jl at 81a402d16204ac9fab65383cdb101b576d96e189 · Albatross-Kite-Transport/VortexStepMethod.jl · GitHub

1 Like

Oh I never noticed that, thanks for mentioning!

Thanks for the reply. This is a discrete system which is piecewise continuous. Alas, the equations of motion are too complex for symbolic notation in general, and would be too slow probably too. This confirms my gut instincts here though.
I’ve spent much of my career leading systems in C++ for space modelling and optimization, and I am doing some prototyping in Julia to understand the possibilities and having a blast. I may go to JuliaCon 2025 and present what I’ve been working on, since it is relatively nearby to where I live. If you attend, I may reach out with some other questions! Thanks again.

1 Like

You may be interested in StructArrays.jl. It’s a flyweight pattern that can combine mutable and immutable arrays to look like structs. Julia’s idiom is have-your-cake-and-eat-it-too. StructArrays would separate mutable and immutable in memory, which would help translation lookaside buffer (TLB) traffic. If the locality is wrong, though, it would be a step backwards. Note that Julia’s profiler is very helpful!

And you are welcome at Pittsburgh Julia meetups, as is everyone. Look for pittsburgh-local channel on Julia slack, or message me.

Generally symbolic is always going to be faster at runtime because it can do tricks that you cannot really do by hand (SCC decompositions etc.). But the compilation starts to slow down (with current tools) at around 10,000 equations. That said, you can usually cache the compilation in a modeling package, and that will usually be the way to get the leanest binary unless you’re doing PDE discretizations where you want to have a bit more control over the looping structure, or you’re doing some large graph equation where you want to again exploit repeated structure.

Hi Patrick, thanks for the reply. There’s a lot to think about here.. I’ll read the reference object identity, I skimmed it and there’s some valuable thinking there.
Your points touch on the number one struggle I am having with Julia by far; it feels like structs are second class citizens, and the language is fighting me. Structs can’t inherit, composition is complicated too, and allowing a struct to be mutable presents performance problems. After a month or so, I have come to love multiple dispatch, but these other constraints are a massive paradigm shift for me. Perhaps I just don’t get it yet though and need to change my mindset.

In the example here, there are many models for time, state, mass etc., assuming one knows what is needed by a user will limit the system. That said, I have made some design changes over the last few weeks that aren’t illustrated in the code snippet I showed, and I may be able to switch to concrete types. I’m still working on that though. I am trying to develop an interface that is generally useful to the aerospace community (a tall order I’ll admit), that is intuitive, and using structs/objects is the norm for those user interfaces.
BTW, good insight on the fact that Spacecraft appears to have state, while a CelestialBody doesn’t. It is a bit odd at first, in these simulation environments, CelestialBodies do have a state, but it is computed from table looks ups. Perhaps a way to think about the abstraction, is that the spacecraft state is an “ODEState”, while most of the time, but not always (estimation is a exception), the celestial body states are assumed known and computed via interpolation using a time that comes from context. Something for me to think about though.


Interesting, i would not expect symbolic to be faster! But the limited experience I have in symbolic coding is years ago in MATLAB, and I don’t know the state of the art. There are definitely cases in the domain where equations of motion can be expressed symbolically. Especially for lower fidelity models used in early design (circular restricted three body, etc). As fidelity increases, particularly for models used operationally, the eoms are pretty messy. I put the EOMs in a fig. The direct harmonic gravity term requires Legendre polynomials requiring recursion for efficient evals, third body gravity requires and table look ups and/or splines. The density in the atmospheric drag looks like this! (https://map.nrl.navy.mil/map/pub/nrl/NRLMSIS/NRLMSISE-00/NRLMSISE-00.FOR) So, some, lend themselves to symbolic expression, others may not, but I am not anywhere near knowing the state of the art. I think for lower fidelity design, based on performance comments above, symbolics may have more promise than I realized. Am I missing something about symbolics that enable general case for the eoms above? I think the stressing here is not the number of EOMS or variables, it is the complexity of the ODEs themselves.

The key isn’t that the symbolic operations are fast. The key is that you can use symbolic arithmetic to generate the mathematical functions to solve, then do tricks to simplify the equations and generate things that are numerically faster and more robust than the standard techniques you’d do by hand (exploiting strongly connected components, homotopy, etc.), and then codegen that optimized form. If you then cache that generated code (you can do this for example using Julia’s package precompilation), it can be very hard to beat for cases that have irregular structures and need implicit methods. It can be extra machinery and overhead for cases that are just explicit, or have lots of structure to exploit (graphs and PDEs) though.

1 Like

Ah, got it. thanks for explaining that. Makes sense now.

Structs can inherit behavior, although they can’t inherit data (fields). However, there are multiple packages, which provide macros that effectively inherit data, see, e.g., CompositeStructs.jl and the links therein.

I think I never used a language where I experienced composition easier as in Julia. What complications do you experience specifically?

I wouldn’t say in general, that mutable structs have performance problems.

While in regular use, they are identical to the typical C++ object (if you have comparable “static” code and avoid the “too dynamic” things which are possible with Julia).
Obviously, when being created they are more like heap-based C++ objects (created with new) than the stack-based C++ objects (but there are cases where the compiler optimizes the creation to be more like the latter one and these cases get more frequent over the Julia versions).
And then, there is the garbage collector, but that is only relevant at the true end of the objects’ life which only manifests if you have allocations during your computation phase (your problem sounds like these can be avoided by allocating everything in the startup phase).

However, as mutable structs implicitly have a pointer due to their object identity, they are space-inefficient for small structs. A struct with two Float32 values would double in size on the relevant hardware when made mutable (if we ignore the meta data Julia uses for object handling). As the pointer is generally stored somewhere else compared to the fields, they are also cache-inefficient and therefore time-inefficient (depending on the specific situation, of course).

Again, this is the same for the typical C++ object working with references and pointers. In that sense you can think of mutable structs in Julia as the analogy of the typical C++ class. What they lack, however, is the efficiency of the algebraic data types typically found in functional programming, especially when the data types are only read. In larger data types, their modification performance leaves to be desired, because in general a new object needs to be constructed with each modification.

In that sense, Julia has two totally different data types and in retrospect it might be unfortunate, that they are distinguished by only the presence of the mutable keyword, which implies that they only differ in “mutability” whereas they are much more different. You can look at Julia’s mutable struct as the procedural data type and the immutable struct as the functional data type, both having their strengths and weaknesses. It is tempting to to try to combine their strengths better, but up to now it is not fully clear how to do this (see e.g. this discussion and the links therein).

By the way, you might want to consider using Unitful.jl. In my experience it’s nice, but not necessary, when having clear variable names and avoiding SI prefixes. But as you use SI prefixes, you will probably benefit from its additional type safety.