In this blog series, I'll be documenting my process of building a minimal chemical molecule drawer (similar to ChemDraw). My motivation is straightforward: I want to combine my background in competitive chemistry with my goal to stress-test Domain-Driven Design in a complex domain.
To provide some context, a chemical drawer is, in essence, a tool that allows users to draw atoms and the bonds between them to generate structures like this one:
This first part focuses strictly on a constrained MVP: generating a canvas with Carbon atoms and single bonds.
The Domain
My domain consists of three core concepts: Atoms, Bonds and Molecules.
Modeling atoms was straightforward: they are entities with an atomic element and a position on the canvas. Bonds, however, involved a more challenging model.
Note: Currently, atoms hold a 2D position. While true DDD would isolate spatial geometry from UI canvas coordinates, I'm keeping simple x, y coordinates in the entity for now to ease the initial rendering setup, treating it as spatial domain data rather than pure UI data.
A philosophical question I faced was: Should bonds be entities or value objects? If atoms are entities, why couldn't bonds be?
After all, bonds, just like atoms, can be created, deleted, and modified.
Ultimately, I modeled them as value objects. Here is why: A bond is entirely described by its attributes (atoms it connects, the bond type, etc.). Textbook value object, I know, but it wasn't immediately obvious to me at first.
The realization hit me when I considered identity. Two atoms can be at the same position (on the canvas) and be the same chemical element, yet they are two different atoms. Bonds are different. There can't be two bonds connecting the same pair of atoms (well, technically they can in real chemistry, but in this model, we treat multiple connections as a single bond with a 'double' or 'triple' type). Additionally, there are no major issues if, instead of modifying a bond, the old one is destroyed and a new one is created.
To wrap up the bond model, I added the following invariant: An atom can't be connected to itself.
Finally, the aggregate root: the Molecule. This entity stores both atoms and bonds. It has methods to create atoms (that will be part of the molecule) and to add bonds.
The Molecule model will ensure for each bond that the two atoms it links exist, they are distinct, and there isn't already a bond between those same atoms.
export class Molecule extends AggregateRoot {
private _atoms: Map<EntityId, Atom> = new Map();
private _bonds: Bond[] = [];
// ...
public addBond(
atomAId: EntityId,
atomBId: EntityId,
type: BondType = BondType.Single,
): Result<Bond, Error> {
const atomA = this._atoms.get(atomAId);
const atomB = this._atoms.get(atomBId);
if (!atomA || !atomB) {
return err(
new Error("Both atoms must exist in the molecule to create a bond"),
);
}
if (atomAId === atomBId) {
return err(new Error("A bond cannot connect an atom to itself"));
}
const exists = this._bonds.some(
(b) =>
(b.atomIds[0] === atomAId && b.atomIds[1] === atomBId) ||
(b.atomIds[0] === atomBId && b.atomIds[1] === atomAId),
);
if (exists) {
return err(new Error("A bond already exists between these atoms"));
}
const bondResult = Bond.create([atomAId, atomBId], type);
if (bondResult.isErr()) {
return err(bondResult.error);
}
const bond = bondResult.value;
this._bonds.push(bond);
return ok(bond);
}
}
With this Molecule aggregate root, the app successfully isolated the basic rules of chemistry from the UI. The domain now dictates that atoms must exist to be bonded, and self-bonding is structurally impossible. Bonds, treated as Value Objects, keep our mental model clean and our memory footprint light.
But right now, the domain is too permissive. A carbon atom could technically accept a hundred single bonds if the UI allowed it (a chemist would cry if they saw this).
In the next parts, I'll be introducing valence warnings, new elements, and more bond types (double, triple).
Stay tuned.


Top comments (0)