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

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).