Levelbrook Labs

Building Null: Notes on Absence

Try the interactive demo

Most of our work as engineers is additive. We build systems to capture, store, relate, and present information. A database schema is a model for things that *are*. A user interface is a canvas for data that *exists*. But what if we invert the problem? What does it mean to build a system whose primary purpose is to represent, and interact with, *absence*?

This isn't just about deleting records. Deletion is a binary, often destructive, operation. I'm interested in the more nuanced problem of modeling absence as a first-class state. Imagine a collaborative environment where the main interaction isn't creating content, but creating *void*. This thought experiment forces us to reconsider fundamental assumptions about data modeling, real-time systems, and user interaction. It's a surprisingly fertile ground for interesting architectural decisions.

Modeling the Void

How do you store nothing? The immediate answer is you don't. But in a multi-user system, the *act* of creating nothing—of nullifying a piece of state—is itself a piece of information. It has a protagonist (who did it) and a timestamp (when it happened). This implies an event-based model. Instead of storing the state of a thing, we store the history of its nullification.

Let's imagine a simple 2D grid, a "canvas of potential." Each cell can either exist or be nulled. A naive approach would be to store a giant matrix, but that's inefficient. A better way is to only store the coordinates of the cells that have been nulled. This is a sparse set representation.

In a relational database like PostgreSQL, this translates into a simple, powerful table. The table doesn't store the grid; it stores the "holes" in the grid.

CREATE TABLE null_points (
    id          BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
    canvas_id   BIGINT NOT NULL REFERENCES canvases(id),
    x           INTEGER NOT NULL,
    y           INTEGER NOT NULL,
    nulled_by   BIGINT REFERENCES users(id),
    nulled_at   TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    -- The core constraint: a point can only be nulled once.
    UNIQUE (canvas_id, x, y)
);

This schema is an explicit log of every act of erasure. It's append-only by its nature. The state of the canvas is a projection of this table. This is far more robust than, say, a JSONB column holding an array of coordinates, because it gives us row-level atomicity, referential integrity, and a built-in audit trail.

Real-time Erasure with Hotwire

A user staring at a void expects to see the actions of others instantly. The void is a shared space. This is a perfect use case for a framework that seamlessly bridges server-side logic and client-side updates, like Hotwire over WebSockets or SSE.

The flow is beautifully simple:

  1. A user clicks a cell at `(x, y)`. A request is sent to the server, perhaps a `POST /canvases/:id/null_points` with parameters `x` and `y`.
  2. The Rails controller attempts to create a new `NullPoint` record. The database's `UNIQUE` constraint on `(canvas_id, x, y)` handles race conditions atomically. If two users click the same cell simultaneously, the first write succeeds, and the second fails with a constraint violation, which we can handle gracefully.
  3. Upon successful creation, the controller broadcasts a Turbo Stream message to all subscribers of the canvas channel.
  4. This message targets the specific cell's DOM ID (e.g., `cell_10_20`) and replaces its content with the "nulled" state template—perhaps just an empty `div` with a different background color.

Here's what the broadcast logic might look like in a Rails model callback:

class NullPoint < ApplicationRecord
  belongs_to :canvas
  belongs_to :nulled_by, class_name: 'User'

  after_create_commit :broadcast_nullification

  private

  def broadcast_nullification
    canvas.broadcast_replace_to(
      canvas,
      target: "cell_#{x}_#{y}",
      partial: 'canvases/nulled_cell',
      locals: { x: x, y: y }
    )
  end
end

This is it. No client-side state management, no JSON serialization boilerplate. The server remains the single source of truth, and the UI updates reactively. It's efficient and conceptually clean.

Where It Breaks: Scale and Concurrency

This elegant model works well, but a senior engineer's job is to ask, "Where does it fall apart?"

Pragmatism and the Human Element

The chosen data model—an immutable log of nullifications—has a powerful side effect: it's a perfect audit trail. We know exactly who nulled what, and when. This is not an afterthought; it's a fundamental property of the architecture.

This makes implementing moderation trivial. If a user acts maliciously, an admin can "undo" their actions with a single query:

DELETE FROM null_points WHERE canvas_id = 123 AND nulled_by = 456;

We would then need a mechanism to broadcast these "un-null" events to clients. This reveals a pragmatic tradeoff. Our simple model doesn't have a concept of "un-nulling." We could add one by deleting the record and broadcasting a custom "re-fill" event. A more purist, event-sourced approach would be to never delete anything, instead appending an `UnNullEvent`. For V1, the simpler "delete and broadcast" is likely sufficient. It solves the user problem directly without introducing the complexity of a full event-sourcing system.

The lesson here is that a robust data model often provides more than just data integrity; it provides the foundation for human-in-the-loop systems, for safety, and for trust.

Final Thoughts

Exploring the "problem" of building null is a valuable exercise. It forces us away from CRUD-centric thinking and toward models based on events, state transitions, and idempotency. The constraints of representing absence lead directly to an architecture that is more resilient, auditable, and real-time by default.

It's a reminder that sometimes the most interesting technical challenges aren't about adding more features, but about carefully considering how to manage the most fundamental state of all: the transition from something to nothing.