The Infinite Loop Club

The last week has been one of those rare, blissful periods in a project's lifecycle. The big, foundational questions felt settled. The core engine was stable, Views were handling complex queries with ease, and the developer experience felt... right. We were spending our time not on inventing, but on building—creating small, fun applications on top of our own platform to see what it felt like.


To celebrate this milestone, we decided to build a "Secret Santa" generator for our own team. It was the perfect small project: a few users, a simple piece of logic to assign recipients, and a UI to show each person their match. The data modeling seemed trivial. We'd have a list of users in our state, and each user object would get a new property, recipient, which would be a direct reference to another user object.


The code was clean. It was intuitive. It perfectly represented the real-world relationships. We ran the script.


The server didn't crash. It did something much weirder. The fans on the architect's machine began to spin up, getting progressively louder. The UI was frozen. We checked the server's process list and saw the CPU pegged at 100%. Confused, we opened the append-only log file that acts as our persistent database. It was growing. Fast. Megabyte after megabyte of gibberish was being written to the disk every second.


We had created a serialization black hole.


Our simple, file-backed database worked by serializing any changed object to a JSON string and writing it to the log. When we created the circular reference, the serializer got stuck in an infinite loop. It started serializing userA, which led it to userA.recipient (userB), which led it to userB.recipient (userC), which led it to userC.recipient (userA), and on and on, forever. The server was dutifully trying to write an infinitely nested object to a file.


This is a problem that traditional databases solved long ago, precisely by _not_ allowing this. In a SQL database, you would never write userA.recipient = userB. You'd use foreign keys. You'd have a recipient_id column that stores the ID of the other user. The relationship is managed through identifiers, not direct object references. This avoids the circular reference problem, but at the cost of developer experience. You can no longer just write user.recipient.name. You have to perform a separate query, a JOIN, to stitch the data back together.


For a moment, we considered giving in. The "obvious" fix was to adopt the old way: add recipientId to our user object and break the beautiful, direct link. The room went quiet. It felt like a total betrayal of our core philosophy. We had spent months eliminating the friction between the developer's mental model and the database's physical model, and now we were contemplating putting the wall back up, all because of a Secret Santa app.


We refused. The tool must bend to the developer, not the other way around.


The problem wasn't the data model; it was our naive serializer. The solution, born from a frantic whiteboarding session, was to make our database understand the concept of a graph.


We rebuilt the serialization engine from the ground up. Now, when a transaction begins, the serializer maintains a map of all the objects it has already processed. The first time it sees an object, it gives it a temporary ID and serializes it fully. If it ever encounters that same object again within the same transaction—as it would in our userC.recipient = userA case—it doesn't try to serialize it again. Instead, it writes a simple pointer, a special token like {"$ref": "temp_id_of_userA"}.


When the server starts up and reads the log to rebuild its state, it does it in two passes. First, it deserializes all the objects. Then, it makes a second pass to "re-hydrate" the graph, replacing all the $ref pointers with direct references to the actual, in-memory objects.


We ran the Secret Santa script again. It worked. Instantly. The log file contained three clean user objects and a few simple pointers. We had built a database that could natively persist and restore a complex object graph, circular references and all. You can now model your data exactly as you think about it, without ever worrying about the limitations of the storage layer. You can have users that are friends with each other, posts that link to users who link back to the posts, and it all just works.


We had stared into the abyss of compromise, and on the other side, we found a database that felt even more magical than before.