Building Null: Notes on Absence
Try the interactive demoMost 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:
- 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`.
- 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.
- Upon successful creation, the controller broadcasts a Turbo Stream message to all subscribers of the canvas channel.
- 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?"
- Canvas Size: A 10,000x10,000 grid is 100 million cells. Rendering this in the DOM is impossible. The solution is viewport virtualization—only rendering the portion of the grid currently visible to the user. This moves complexity to the client-side, requiring a bit of JavaScript to manage the viewport and dynamically load cell states.
- Data Density: If a canvas becomes heavily nulled, the `null_points` table could grow to millions or billions of rows. Fetching all points for the initial render becomes a bottleneck. Proper indexing on `(canvas_id, x, y)` is critical. For extremely large datasets, we might need to query for points only within the user's viewport, requiring a spatial index or at least range queries on `x` and `y`.
- Broadcast Storms: On a very active canvas with thousands of concurrent users, broadcasting every single nullification to every single user is wasteful and can saturate the server's network capacity. A more sophisticated approach would be to shard the broadcast channels, perhaps by grid region. A user only subscribes to updates for the regions they are currently viewing.
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.