Skip to main content

Relationship Map

The ODSRelationMap class collects the entities, value objects and relations in scope. Where DDD has no notation of its own, ODS uses UML, so relationMapToDigraph draws the map as a UML class diagram:

  • each entity or value object is a class box with a stereotype («root entity», «entity» or «value object»), its name and an attribute compartment, with identity attributes marked {id};
  • references is a navigable association (solid line, open arrowhead), includes is a composition (filled diamond on the owner) and uses is a dependency (dashed line, open arrowhead);
  • the relation label sits on the line and the cardinality at the target end;
  • classes are grouped in one cluster per aggregate, labelled with its domain, subdomain and bounded context path.

toPlantUML() returns the same diagram as PlantUML class diagram source for teams that already render PlantUML with their own tooling. ODS does not render PlantUML itself, see decision 10 in the repository.

This will produce the following SVG diagram:

relation-map-example.svg

import { writeFileSync } from "node:fs";
import { ODSRelationMap, Workspace } from "@open-domain-specification/core";
import { relationMapToDigraph } from "@open-domain-specification/graphviz";
import { describe, expect, it } from "vitest";

const ws = new Workspace("eCommerce", {
description: "DDD workspace for an eCommerce platform example",
version: "0.1.0",
homepage: "https://example.com",
primaryColor: "#0ea5e9",
logoUrl: "https://example.com/logo.svg",
});

// === DOMAINS ===
const commerce = ws.addDomain("Commerce", {
description: "Core commerce capabilities",
});

const content = ws.addDomain("Content", {
description: "Supporting site content",
});

// === SUBDOMAINS ===
const sales = commerce.addSubdomain("Sales", {
type: "core",
description: "From cart to order",
});

const publishing = content.addSubdomain("Publishing", {
type: "supporting",
description: "Pages and articles",
});

// === BOUNDED CONTEXTS ===
const orderingBC = sales.addBoundedcontext("Ordering", {
description: "Checkout flow and orders",
});

const cmsBC = publishing.addBoundedcontext("CMS", {
description: "Site content retrieval/rendering",
});

// === ORDERING ===
const checkoutSvc = orderingBC.addService("CheckoutApp", {
description: "Application service for placing orders",
type: "application",
});

const orderAgg = orderingBC.addAggregate("Order", {
description: "Immutable record of a purchase",
});

const orderEntity = orderAgg.addRootEntity("Order", {
description: "Order header",
});
orderEntity.addAttribute("id", { type: "OrderId", identity: true });
orderEntity.addAttribute("placedAt", { type: "Instant" });

const moneyVO = orderingBC.addValueObject("Money", {
description: "Amount + currency",
});
moneyVO.addAttribute("amount", { type: "Decimal" });
moneyVO.addAttribute("currency", { type: "ISO 4217" });

orderEntity.uses(moneyVO, "totals", "1");

orderAgg.addInvariant("TotalsNonNegative", {
description: "Order totals must be >= 0",
});

// Checkout exposes an operation to place orders
const placeOrderOp = checkoutSvc.provides("PlaceOrder", {
description: "Create an order from a valid checkout",
type: "operation",
pattern: "open-host-service",
});

// === CMS ===
const rendererSvc = cmsBC.addService("ContentRenderer", {
description: "Application service for fetching/rendering content",
type: "application",
});

const articleAgg = cmsBC.addAggregate("Article", {
description: "Renderable content unit",
});

const articleEntity = articleAgg.addRootEntity("Article", {
description: "Article/page content",
});

const slugVO = cmsBC.addValueObject("Slug", {
description: "URL-safe identifier",
});

articleEntity.uses(slugVO, "addressable by");

articleAgg.addInvariant("SlugFormatValid", {
description: "Slug must be lowercase, dash-separated",
});

// CMS exposes an operation to get content
const getArticleOp = rendererSvc.provides("GetArticle", {
description: "Fetch article/page content by slug",
type: "operation",
pattern: "open-host-service",
});

// === CONSUMES ===
// Ordering needs content (e.g., terms page) during checkout
checkoutSvc.consumes(getArticleOp, {
pattern: "anti-corruption-layer",
});

// CMS might reference latest orders count/snippet for a dynamic page
rendererSvc.consumes(placeOrderOp, {
pattern: "conformist",
});

describe("Relationship Example", () => {
it("should compile ", async () => {
const svg = await relationMapToDigraph(
ODSRelationMap.fromWorkspace(ws),
).toSVG();
expect(svg).toMatchSnapshot();
writeFileSync("./static/img/relation-map-example.svg", svg);
});
});