Skip to main content

ODS Core

The Open Domain Specification (ODS) Core package provides the foundation for modeling complex domains with Domain-Driven Design (DDD) principles. It serves two main purposes:

  1. Schema Types – ODS Core defines the types that make up the JSON Schema specification itself. These types form the authoritative, open standard for describing domains.

  2. TypeScript Toolkit – Beyond the specification, ODS Core offers a set of TypeScript wrappers and classes to make it easier to construct domain models directly in code. These utilities provide a practical, developer-friendly way to create, validate, and share domain models as code.

The advantage of this open approach is that once defined, schemas can be applied across many downstream use cases, such as:

  • πŸ—ΊοΈ Domain Visualization: Tools that visualize domain models as diagrams or graphs
  • πŸ—οΈ Scaffolding: Generating code templates based on domain models
  • πŸ“„ Documentation: Automatically generating documentation from domain models
  • ✨ GenAI: Using domain models to inform AI systems about the high level business intent and how the implementation should align with it

Domain Hierarchy​

The ODS core follows a hierarchical structure aligned with Domain-Driven Design principles:

Workspace
β”œβ”€β”€ Domain (problem space)
β”‚ └── Subdomain (core/supporting/generic)
β”œβ”€β”€ Bounded Context (solution space; serves the subdomains it lists)
β”‚ β”œβ”€β”€ external / bigBallOfMud / (three kinds of unknown, mutually exclusive: a system we do not own,
β”‚ β”‚ boundaryOnly one of ours we cannot read, one of ours nobody has interviewed yet)
β”‚ β”œβ”€β”€ Service (application/domain)
β”‚ β”‚ β”œβ”€β”€ Consumables (event or operation; may be internal; an operation may declare returns, rejects and reasons)
β”‚ β”‚ └── Consumptions
β”‚ β”œβ”€β”€ Glossary Term (ubiquitous language, optionally embodied by an element)
β”‚ β”œβ”€β”€ Value Object (with attributes) (a value of the context's language; any aggregate may hold one)
β”‚ β”‚ └── Invariants (rules the value keeps by construction)
β”‚ β”œβ”€β”€ Schema (payload shape with attributes, shared by consumables; a published kind on an external context)
β”‚ β”œβ”€β”€ Policy (on event consumables β†’ then operation consumables)
β”‚ β”œβ”€β”€ Process (starts/on/then/ends; holds state across events, unlike a policy)
β”‚ β”‚ └── Deadlines (after, from) (a time limit the process raises to itself)
β”‚ β”œβ”€β”€ Context Invariant (a rule across instances or aggregates of this context; names its guard)
β”‚ └── Aggregate
β”‚ β”œβ”€β”€ Entities (with attributes)
β”‚ β”œβ”€β”€ Invariants
β”‚ β”œβ”€β”€ Consumables (event or operation; may be internal, carry a schema, and operations raise events)
β”‚ └── Consumptions
β”œβ”€β”€ Team (owns bounded contexts)
└── Context Relationship (upstream-downstream, customer-supplier, partnership, shared-kernel, separate-ways)

Domains and subdomains describe the problem space. Bounded contexts describe the solution space and are owned by the workspace, each linked to the subdomains it serves, so one context can span several subdomains and one subdomain can be served by several contexts.

A good number of things this tree does not show β€” no delivery flag, no modules, no actors, no read-model element, no operations on a value object, an entity has one home, a context invariant records the check rather than the store β€” are left out on purpose, not because DDD forbids them. See Tactical Design and the repository's decisions/ folder, starting with decision 15, for the reasoning behind each one.

Identity and refs​

Every element has an id that becomes its key in the JSON document and the last segment of its ref, for example #/boundedcontexts/sales/aggregates/order. When you omit id in the DSL it is derived from the name ("Order Line" becomes order_line); pass id explicitly when a name is likely to change and other elements point at it. When a document is loaded, the JSON keys are the ids, so a document round-trips regardless of how its names are spelled.

Refs never embed the domain or subdomain of a bounded context, so renaming those never breaks a ref.

Reference models​

The models/ workspace folder in the repository holds five packages, each a workspace written with the DSL and generated into a .ods/*.json file used by the viewer, the export and the test fixtures:

  • Swagger Petstore is the demonstration reference. Every feature of the model appears once, descriptions say why each choice was made, and it validates clean. Read this one first.
  • RiverMart, a fictional online marketplace, StreamLine, a fictional streaming service, and NorthBank, a fictional retail bank, are stress models: large enough to exercise the pages, the diagrams and the validation, with a legacy big ball of mud, shared kernels and partnerships, deep aggregates, and a few deliberate structural problems so diagnostics have something to show. Between them the three trigger every rule in the catalog.
  • Clinic, an outpatient clinic, is modelled blind from the skill and the docs alone, with no other reference model read first: it is the check that the guidance in this repository is enough on its own to produce a sound workspace.

Each fictional organisation comes with a BRIEF.md describing the business and a DISCOVERY.md recording the interviews and event-storming session the model was drawn from, so every context, relationship, invariant and policy can be traced back to something someone said.

Installation​

npm install @open-domain-specification/core

Building a Domain Model​

Using the Typescript API you can build domains using Typescript so they can be maintained as code.

Finally, once the domain model is defined, it can be exported as a JSON document and used for visualization, documentation, or other purposes.

import { Workspace } from "@open-domain-specification/core";
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",
});

const moneyVO = orderingBC.addValueObject("Money", {
description: "Amount + currency",
});

orderEntity.uses(moneyVO, "totals");

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("Visitor Example", () => {
it("should compile ", () => {
expect(ws.toSchema()).toMatchSnapshot();
});
});