The Guitar as a Computational System
What building Guitar Atlas — a music theory visualizer in React + TypeScript — taught me about domain modeling, DDD, and encoding implicit knowledge.
· 6 min read
A Deterministic Model of Music Theory #
From Frets to Functions: Building a Music Theory Visualizer in React + TypeScript
Some of the hardest domain modeling problems I’ve faced didn’t come from distributed systems or business logic. They came from a side project about guitar.
Every guitarist knows the feeling: you can play a scale shape across the fretboard, but you don’t really know what it is. Why it works. What chords it implies. What changes when you move it up a fret.
I found a partial answer in Mike Hadlow’s gtr-cof, a simple tool mapping scales across the circle of fifths and fretboard. Seeing notes light up across multiple views at once changed how I thought about the instrument.
But I wanted more control: more scales, more flexibility, and a system I could extend. As a developer and guitarist, building it felt like the best way to actually understand it.
So I built Guitar Atlas: a React + TypeScript app with three synchronized views (a chromatic circle, a circle of fifths, and a guitar fretboard) powered by a custom music theory engine.
What I didn’t expect was how much resistance the domain would give back.

C Dorian scale selected: scale tones and their interval labels light up simultaneously across the chromatic circle, circle of fifths, and fretboard. One selection, three projections.
Music theory looks simple: twelve notes, a few scale formulas, some chord types. But when you try to encode it precisely, you quickly discover that musicians rely on a large amount of implicit knowledge the computer refuses to assume.
The Core Problem: Music Theory Is Mostly Implicit #
The first challenge was realising that nothing in music theory is naturally “stored.” Almost everything is derived.
Intervals, scales, and chords only make sense relative to each other. So instead of modelling them as static data, I treated them as immutable primitives and built everything from relationships.
A major 3rd is always a major 3rd: fixed semitone distance, fixed identity. Scales are just interval vectors. For example, major is:
[0, 2, 4, 5, 7, 9, 11]Transposition is just modular arithmetic. The circle of fifths works because +7 semitones cycles through all 12 notes, a structural property, not a UI choice.
This leads to the key design decision:
Core Principle Nothing musical is stored. Everything is derived.

D major scale on the chromatic circle: the 7 scale tones are highlighted, each wedge showing its interval label (R, M2, M3, P4…) inside and note name in the middle ring. The outer ring shows the chord quality and Roman numeral for each degree, all derived, nothing stored.
Chords and Harmony Are Emergent #
The biggest shift in the model was removing chord definitions entirely.
Instead of storing things like “Cmaj7 = C E G B,” the system builds chords dynamically:
- take scale degrees
- stack thirds
- compute semitone gaps
- infer quality from structure
From this, all chord types emerge consistently across all keys and scales. Roman numerals also fall out automatically; there is no special-casing for “IV in major is major.”
This makes the system extensible by default. Adding seventh chords didn’t require new rules, only extending the derivation step.
On Harmony Harmony is not data. It is a result of structure.

A chord is selected (IV, F major in C major): chord tones are highlighted with a distinct colour across all three views, the Roman numeral and quality displayed on the circle. No lookup table; quality and voicing are derived from stacking thirds over the scale degrees.
The Hardest Problem: Naming Notes Correctly #
Once the system worked structurally, spelling became the hardest part.
A single chromatic index (0–11) is ambiguous. C# and D♭ share pitch but not meaning. Musicians care deeply about letter structure, not just sound.
So the model separates:
- chromatic position
- diatonic letter context
This allows the system to preserve musical spelling rules instead of collapsing everything into numbers.
Behind the scenes, a mapping strategy encodes how musicians naturally prefer naming intervals, ensuring that scales like D minor pentatonic are spelled correctly (D F G A C, not E# or F## variants).
This is where the domain really pushes back: correctness is not about sound, but about meaning.
The Architecture: One Pipeline, One Truth #
Once the domain stabilised, the architecture became straightforward.
The entire system reduces to a single pipeline:
AppState → musicContext → computeNotes → NoteNode[] → UI + Audio
The single data pipeline: AppState feeds a musicContext selector, which passes only the 5 music-relevant fields into the pure computeNotes engine. It returns 12 NoteNode objects that fan out to the three UI views and the AudioEngine. No other data paths exist.
Each part has a strict responsibility:
| Stage | Responsibility |
|---|---|
AppState | User intent (scale, tonic, chord, tuning) |
musicContext | Minimal projection for the domain |
computeNotes | Pure music engine |
NoteNode | Rendering contract |
| UI + Audio | Projections of the same result |
The most important abstraction is computeNotes. It is the only place where music theory exists.
It returns exactly 12 NoteNode objects, one per pitch class, containing only what the UI needs:
1interface NoteNode {
2 pc: string; // spelled pitch class — "F#", "Bb"
3 intervalLabel: string; // "R", "m3", "P5", "M7"
4 isInScale: boolean;
5 isInChord: boolean;
6 isTonic: boolean;
7 chordRomanNumeral: string; // "I", "ii°", "V7"
8}The UI never sees scales or intervals directly. It only consumes this derived structure.
This separation made the system predictable: if something is wrong, there is exactly one place to debug.
UI as Projection, Not Logic #
The frontend consists of three synchronized views: a chromatic circle, a circle of fifths, and a fretboard.
They are not coordinated explicitly. They all render from the same 12 NoteNode objects.
This makes synchronisation free: no cross-component messaging, no state duplication.
The complexity instead shifts to rendering geometry (especially SVG layout, circular positioning, and fretboard mapping) but that complexity is isolated from the domain.
The boundary is doing its job: it doesn’t remove complexity, it contains it.
The Fretboard and Audio Layer #
The fretboard is a deterministic lookup: each string/fret position maps to a chromatic index, then to a NoteNode.
Audio follows the same principle. An AudioEngine interface receives only MIDI numbers:
1interface AudioEngine {
2 playNote(midi: number): void;
3 playChord(midi: number[]): void;
4 playScale(midi: number[]): Promise<void>;
5}No musical concepts cross into audio. The system only translates once, at the boundary.
Clicking a note plays it. Clicking a chord plays its derived structure. Watching and hearing reinforce each other, turning abstract theory into feedback.
What Building This Taught Me #
The biggest lesson wasn’t about music; it was about modelling.
Three ideas stood out:
Find invariants first. Intervals, scales, and chord structure are stable. UI is not. The system should reflect that hierarchy.
Derive everything possible. If you can compute it reliably, you should not store it. Stored knowledge is where inconsistency appears.
Separate meaning from rendering. The UI should never interpret the domain. It should only display the result of a computation.
Once those boundaries were in place, the system stopped fighting itself.
What started as a guitar tool became a lesson in how far a clean domain model can go when it is allowed to fully express itself.
And I now understand the fretboard differently, not as shapes to memorise, but as a structured system I had to rebuild from first principles to make it computable.