Diagram9 project
Diagram9 v1/v2: Structurizr clone & D2 parser in Go
Personal

Diagram9 v1/v2: Structurizr clone & D2 parser in Go

Feb 2022
Table of Contents

Overview

Before arriving at the current hybrid architecture of v3, Diagram9 went through two distinct phases of failure and learning.

v1: Structurizr with Modern UI

Diagram9 v1 began as an attempt to modernize the Structurizr experience. While Structurizr is a powerful “Diagram as Code” tool based on the C4 model, its default web interface felt dated.

My goal was to build a sleek, modern React frontend that could still leverage the robust Structurizr DSL for defining architecture.

Architecture

The system consisted of:

  1. Frontend: A React application using monaco-editor for code editing.
  2. Backend: A Node.js server that spawned a Java subprocess to run the Structurizr CLI.
  3. Rendering: The Java process would parse the DSL and return a JSON representation, which the frontend would then render.

Why I Moved On

While functional, this architecture was heavy. Relying on a Java runtime made deployment complex and slow. Additionally, I found the C4 model somewhat restrictive for general-purpose diagramming, which led to me explore D2.

v2: My Own D2 Parser in Go

In v2, I pivoted to D2 as the language of choice due to its simpler syntax and powerful layout engines. However, I fell into a classic engineer’s trap: I prioritized my desire to learn new technology over shipping a working product.

A Perfect Opportunity and the “Technology Over Product” Trap

At the time, I happened to be interested in learning Go and wanted to dive into interpreter and compiler design. I had been planning to read Thorsten Ball’s Writing An Interpreter In Go and Writing A Compiler In Go.

I stumbled upon the D2 repository, liked the language, and noticed it happened to be written in Go. I saw this as the perfect opportunity to kill two birds with one stone: learn these skills and solve the core problem of my project.

I decided to implement a full Recursive Descent Parser (and a Pratt Parser for expressions) in Go, mirroring the architecture I learned from the books and referencing the D2 source code to understand how they implemented their parser in Go.

// A snippet of the 1,500+ lines of Go parser logic
func (p *Parser) parseBlockStatement() *ast.BlockStatement {
	block := &ast.BlockStatement{Token: p.curToken}
	p.nextToken()

	for !p.curTokenIs(token.RBRACE) && !p.curTokenIs(token.EOF) {
		stmt := p.parseStatement()
		if stmt != nil {
			block.Statements = append(block.Statements, stmt)
		}
		p.nextToken()
	}

	return block
}parser.go

I soon found myself in an endless cycle of patching and breaking. I would implement support for one D2 example, only to find that my parser completely choked on the next one due to missing language features or subtle syntax variations. Every “fix” revealed yet another edge case I hadn’t anticipated.

This effort quickly ballooned to over 1,500 lines of complex parsing logic. I was essentially rewriting the D2 compiler’s lexical analysis and parsing phases from scratch with a less than optimal implementation.

The Reality Check

I completely underestimated the complexity of D2’s grammar. Features that looked simple on the surface, like Edge Groups ((a, b) -> c) or Map Keys (x.y.z: value), required sophisticated lookahead and backtracking logic that my naive parser couldn’t handle.

For context, here is a snippet of the actual D2 parser handling edge groups. It manages state flags, performs recursive parsing, and handles complex error recovery—nuances I completely missed.

// The actual D2 parser handling edge groups (from the official repo)
func (p *parser) parseEdgeGroup(mk *d2ast.Key) {
	// To prevent p.parseUnquotedString from consuming terminating parentheses.
	p.inEdgeGroup = true
	defer func() {
		p.inEdgeGroup = false
	}()

	src := p.parseKey()
	p.parseEdges(mk, src)

	r, newlines, eof := p.peekNotSpace()
	if eof || newlines > 0 {
		p.rewind()
		return
	}
	if r != ')' {
		p.rewind()
		p.errorf(mk.Range.Start, p.pos, "edge groups must be terminated with )")
		return
	}
	p.commit()
    // ... handling edge indices and keys ...
}d2/d2parser/parse.go

My implementation was a pale shadow of this robust logic, riddled with bugs and unable to handle even valid D2 syntax correctly.

Why It Failed

  1. Complexity Explosion: D2 is not just simple boxes and arrows. As I discovered in the official test suite, features like Edge Groups (q.(x -> y).z: (rawr)) and complex Map Keys (x.(z->q)[343].hola: false) required sophisticated lookahead and backtracking logic that my naive parser simply couldn’t handle.
  2. Lack of Visual Parity: My custom Scene Graph (constructed via React components) never achieved fidelity with D2’s reference renderer. Replicating the exact Box Model, typography, and Compositing rules of D2’s engine was nearly impossible.
  3. Inferior Layout Engine: D2 utilizes sophisticated Layout Engines (like ELK and Dagre) for Orthogonal Routing and collision avoidance. My naive implementation using simple Bezier curves resulted in cluttered and overlapping edges.
  4. Maintenance Burden: Every time D2 released a new feature (like “sketch mode” or new shapes), I had to reverse-engineer its rendering logic. I was spending 90% of my time maintaining the Rendering Pipeline and 10% on the actual app.

The Lesson

v2 taught me a valuable lesson in Technology over Product. I realized that I was more interested in the technology (learning Go, writing a compiler) than in actually solving the user’s problem.

While building a custom parser was an incredible learning experience, it was complete overkill and, more importantly, would have taken an infinite amount of time to complete. I fell into the trap of reinventing the wheel for the sake of intellectual curiosity, resulting in a product that was complex, fragile, and harder to maintain.

This failure directly led to the Hybrid Architecture of v3, where I prioritized pragmatism and leveraged existing tools to deliver value.

Related Projects

    Mike 3.0

    Send a message to start the chat!

    You can ask the bot anything about me and it will help to find the relevant information!

    Try asking: