The process follows four distinct phases.
Phase 1: Topology Definition
The foundation is a hierarchical tree of components. Layout direction and dependency relationships are declared as props on the base component. The renderer interprets these props to calculate spatial arrangement and connection routing.
import { Shape } from '@dinghy/base-components'
export default () => (
<Shape>
Payment Gateway
<Shape _direction='horizontal'>
<Shape _dependsOn='Auth Service'>API Router</Shape>
<Shape _dependsOn='Payment Processor'>Auth Service</Shape>
<Shape _dependsBy='Auth Service'>Payment Processor</Shape>
</Shape>
<Shape _direction='vertical'>
<Shape _dependsOn='Ledger DB'>Transaction Engine</Shape>
<Shape _dependsBy='Transaction Engine'>Ledger DB</Shape>
</Shape>
</Shape>
)
Architecture Rationale: The tree structure mirrors actual system boundaries. Horizontal grouping aligns components that operate in parallel or share a network zone, while vertical grouping stacks tightly coupled services. Explicit _dependsOn and _dependsBy props eliminate ambiguous routing. The renderer calculates connection anchors automatically, removing manual line-drawing.
Phase 2: Semantic Abstraction
Raw topology trees become difficult to maintain as systems grow. Abstracting layout primitives into named components improves readability and enforces consistency. Each named component encapsulates its role, default dependencies, and styling preferences.
import { Shape } from '@dinghy/base-components'
const Router = (props: any) => <Shape _dependsOn='AuthService' {...props} />
const AuthService = (props: any) => <Shape _dependsOn='PaymentEngine' {...props} />
const PaymentEngine = (props: any) => <Shape _dependsBy='AuthService' {...props} />
const Ledger = (props: any) => <Shape _dependsBy='TransactionEngine' {...props} />
const TransactionEngine = (props: any) => <Shape _dependsOn='Ledger' {...props} />
export default () => (
<Shape>
Financial Core
<Shape _direction='horizontal'>
<Router />
<AuthService />
<PaymentEngine />
</Shape>
<Shape _direction='vertical'>
<TransactionEngine />
<Ledger />
</Shape>
</Shape>
)
Architecture Rationale: Named components separate structural intent from layout mechanics. The diagram source reads like a system manifest rather than a coordinate map. Props spreading ({...props}) allows downstream overrides without breaking encapsulation. This pattern scales cleanly to multi-team architectures where each squad maintains its own component library.
Phase 3: Visual Standardization
Generic boxes communicate structure but lack domain context. Importing provider-specific icon libraries replaces placeholder shapes with recognized resource symbols. The TSX structure remains identical; only the rendered output changes.
import * as awsGeneralResources from '@dinghy/diagrams/entitiesAwsGeneralResources'
const Router = (props: any) => (
<awsGeneralResources.Client _dependsOn='AuthService' {...props} />
)
const AuthService = (props: any) => (
<awsGeneralResources.SecurityIdentityCompliance _dependsOn='PaymentEngine' {...props} />
)
const PaymentEngine = (props: any) => (
<awsGeneralResources.Analytics _dependsBy='AuthService' {...props} />
)
const Ledger = (props: any) => (
<awsGeneralResources.Database _dependsBy='TransactionEngine' {...props} />
)
const TransactionEngine = (props: any) => (
<awsGeneralResources.Compute _dependsOn='Ledger' {...props} />
)
export default () => (
<Shape>
Financial Core
<Shape _direction='horizontal'>
<Router />
<AuthService />
<PaymentEngine />
</Shape>
<Shape _direction='vertical'>
<TransactionEngine />
<Ledger />
</Shape>
</Shape>
)
Architecture Rationale: Icon libraries map directly to cloud provider catalogs, ensuring diagrams align with operational dashboards and runbooks. The separation between logical topology and visual representation allows teams to swap icon sets without restructuring dependencies. This pattern supports multi-cloud diagrams by mixing provider-specific libraries in a single tree.
Phase 4: Deterministic Rendering
The TSX tree is compiled into a structured graph. The layout engine applies force-directed or grid-based algorithms to position nodes, calculate bounding boxes, and route connection lines. The final output is a standard .drawio file containing XML-compliant shape definitions, connection metadata, and styling attributes. Because the renderer is deterministic, identical inputs always produce identical outputs. This eliminates manual cleanup and ensures that diagram generation can be automated in build pipelines.
Pitfall Guide
1. Circular Dependency Loops
Explanation: Declaring mutual dependencies between components creates routing conflicts. The layout engine cannot resolve anchor points when edges form closed loops, resulting in overlapping lines or rendering failures.
Fix: Audit the topology tree for bidirectional _dependsOn/_dependsBy pairs. Convert circular relationships into explicit message queues or event buses, then declare one-way dependencies pointing to the intermediary.
2. Ignoring Layout Direction Hints
Explanation: Omitting _direction props forces the renderer to guess spatial arrangement. Large trees default to unpredictable layouts, causing component overlap and broken connection routing.
Fix: Explicitly declare _direction='horizontal' or _direction='vertical' on every container node. Use horizontal for parallel services and vertical for tightly coupled stacks. Test layout changes incrementally.
3. Over-Abstracting Layout Logic
Explanation: Nesting components too deeply or creating wrapper components that only pass props adds indirection without value. The diagram source becomes harder to trace than the rendered output.
Fix: Limit abstraction to two layers: base Shape components and semantic wrappers. Avoid creating components that only modify layout props. Keep dependency declarations visible at the call site.
4. Mixing Semantic and Structural Props
Explanation: Combining role-specific props (e.g., role='auth') with layout props (e.g., _direction) on the same component creates coupling between business logic and rendering behavior. Changes to one break the other.
Fix: Separate concerns. Use named components to encapsulate semantic meaning. Pass layout props exclusively through container wrappers or explicit configuration objects.
5. Neglecting Draw.io XML Compatibility Limits
Explanation: The renderer outputs standard .drawio XML, but excessive custom styling or unsupported shape attributes can break compatibility with the draw.io editor. Manual edits may fail or corrupt the file.
Fix: Stick to officially supported icon libraries and base component props. Avoid injecting raw XML or custom CSS. Validate output files by opening them in the draw.io editor before committing.
6. Hardcoding Connection Anchors
Explanation: Manually specifying connection points (top, bottom, left, right) overrides the renderer's automatic routing. This works for simple diagrams but breaks when components resize or reorder.
Fix: Rely on the engine's default anchor calculation. Only override anchors when dealing with legacy draw.io templates or specific compliance requirements. Document overrides explicitly.
7. Skipping Component Reusability
Explanation: Duplicating component definitions across multiple diagrams creates maintenance overhead. Updating a service icon or dependency requires changes in dozens of files.
Fix: Extract shared components into a central library. Export them as npm packages or monorepo modules. Import them consistently across all diagram sources. Version the library alongside infrastructure code.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Small team, infrequent updates | Manual draw.io editor | Low overhead, visual flexibility | Low initial, high long-term decay |
| Multi-service architecture, frequent changes | Dinghy TSX-as-Code | Deterministic layout, PR-reviewable diffs | Moderate setup, near-zero maintenance |
| Compliance-heavy environment, strict visual standards | Dinghy + custom icon library | Enforced consistency, automated validation | High initial, low operational risk |
| Cross-cloud hybrid topology | Dinghy with mixed provider libraries | Single source of truth, unified rendering | Moderate complexity, high accuracy |
| Non-technical stakeholders need direct editing | Dinghy output + draw.io editor | Code generation for accuracy, manual tweaks for accessibility | Balanced, preserves ecosystem compatibility |
Configuration Template
{
"name": "architecture-diagrams",
"version": "1.0.0",
"private": true,
"scripts": {
"build": "dinghy render ./src/diagrams --output ./dist",
"watch": "dinghy watch ./src/diagrams --output ./dist",
"validate": "dinghy lint ./src/diagrams"
},
"dependencies": {
"@dinghy/base-components": "^2.4.0",
"@dinghy/diagrams": "^1.8.2"
},
"devDependencies": {
"typescript": "^5.4.0",
"tsx": "^4.7.0"
}
}
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"esModuleInterop": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*.ts", "src/**/*.tsx"]
}
// src/diagrams/core.ts
import { Shape } from '@dinghy/base-components'
import * as awsGeneralResources from '@dinghy/diagrams/entitiesAwsGeneralResources'
const Gateway = (props: any) => (
<awsGeneralResources.Client _dependsOn='AuthService' {...props} />
)
const AuthService = (props: any) => (
<awsGeneralResources.SecurityIdentityCompliance _dependsOn='PaymentEngine' {...props} />
)
const PaymentEngine = (props: any) => (
<awsGeneralResources.Compute _dependsBy='AuthService' {...props} />
)
const Ledger = (props: any) => (
<awsGeneralResources.Database _dependsBy='TransactionEngine' {...props} />
)
const TransactionEngine = (props: any) => (
<awsGeneralResources.Analytics _dependsOn='Ledger' {...props} />
)
export default () => (
<Shape>
Financial Core
<Shape _direction='horizontal'>
<Gateway />
<AuthService />
<PaymentEngine />
</Shape>
<Shape _direction='vertical'>
<TransactionEngine />
<Ledger />
</Shape>
</Shape>
)
Quick Start Guide
- Initialize the project: Run
npm init -y, install dependencies (@dinghy/base-components, @dinghy/diagrams, typescript, tsx), and configure tsconfig.json with JSX support.
- Create the topology file: Add a
.tsx file in src/diagrams/. Import Shape and provider icon libraries. Define your architecture tree using nested components and explicit dependency props.
- Run the renderer: Execute
npx dinghy render ./src/diagrams --output ./dist. The command compiles the TSX tree and generates a .drawio file in the output directory.
- Verify compatibility: Open the generated
.drawio file in the draw.io editor. Confirm that layout, connections, and icons match the source tree. Make manual adjustments if needed; they will persist in the file.
- Automate generation: Add the render script to your CI pipeline. Trigger diagram regeneration on pull requests to ensure documentation stays synchronized with infrastructure changes.