There's an alternative approach which requires rethinking how object associations are represented: instead of storing associations between objects inside the objects themselves (as typically done in OO code) add them afterwards in a separate layer as Maps.
Because all of the nodes in the object graph exist by the time associations are created, immutable bidrectional associations can be easily created using Maps.
scala> class Thing (val name:String)
defined class Thing
scala> class Ref (val name:String)
defined class Ref
scala> new Thing("Thing1")
res0: Thing = Thing@5c2bae98
scala> new Ref("Ref1")
res1: Ref = Ref@7656acfa
scala> val thing2Ref = Map(res0 -> res1)
thing2Ref: scala.collection.immutable.Map[Thing,Ref] = Map(Thing@5c2bae98 -> Ref
@7656acfa)
scala> val ref2Thing = Map(res1 -> res0)
ref2Thing: scala.collection.immutable.Map[Ref,Thing] = Map(Ref@7656acfa -> Thing
@5c2bae98)
If you think about it, this approach is similar to how relational databases work. Multi-valued associations between tables are not stored in the rows themselves, but in separate indexes. Even when association indexes are not present and so a query is resolved using a table scan, it is using the primary key indexes to locate all candidate rows for the result.
An advantage of this style is that associations can be added or changed without affecting the objects themselves, and different associations can be employed for different audiences/use-cases. A disadvantage is that an association map is not directly accessible on instances where the associations begins; it has to be passed around & provided separately.