Graph Ontologies Design from BT, Nature Metrics & NHS England

Graph databases promise a natural fit for complex, relationship-heavy domains. But there’s a successful implementation gap between a promising prototype and a production system that performs, scales, and survives organisational change. This article is based on my personal experience building three real-world graph ontologies in three different organisations. First was BT’s network inventory (SRIMS), second was a species taxonomy and distribute graph at NatureMetrics, and the third is a clinical ontology for NHS England primary care. ·

The three domains were extremely different. Telecoms network topology, biological taxonomy, and clinical terminology share no vocabulary and serve entirely different users. The structural challenges of building graph ontologies across all three were remarkably consistent. The examples below use Neo4j and Cypher, but the principles apply equally to any graph database and traversal language (ncluding Gremlin or openCypher). The following are some reflections on the implementations.


1. Define the Logical Ontology Before Touching the Database

Change in a graph database is difficult to implement, even with a GraphQL query abstraction layer. It is therefore important before going to production to define your nodes and are your relationships.

Graph databases allow the user to start modelling immediately. It is fluid and fun, but like fun fluids it can become sticky later. The flexibility of graph is real, but it masks a trap. An ontology that grows organically from technical convenience rather than domain logic ends up with nodes that are structurally inconsistent, relationships that conflate different semantic meanings, and queries that become increasingly difficult to complete.

BT SRIMS (Service and Resource Inventory Management System) took multiple iterations to define the ontology. Multiple discussions were held on the definition of Network Terminating Equipment and what that meant if the transmission protocol was Satellite, Copper or FTTP. This approach meant that various propietary BT technologies (hello blown fibre) had to be modelled as primary nodes and multiple rdfs:subClassOf and IS_A relationships.

At NatureMetrics, the logical structure for the ontolgy came from biology, specifically the eight major taxonomic ranks (Kingdom, Phylum, Class, Order, Family, Genus, Species, Subspecies). A species detection connects a specific specimen observation to a location and a point in time. The taxonomic hierarchy above species is largely stable and pre-existing. Designing the ontology around those ranks, rather than around the shape of the incoming eDNA data, meant the graph could answer questions at any level of biological specificity without restructuring.

The graph at NHS England followed the SNOMED taxonomy with logical application across primary care data which was imported into Snowflake. The graph could traverse the data by using SNOMED definitions for graph nodes. Defining the ontology as a domain diagram with a subject-matter experts is critical before trying to define property graphs, node labels, or indexes. The logical model is the specification. The physical graph schema is the implementation.


SRIMS: seven information layers from Service to Site/Building, each a distinct node type in Neo4j, connected by typed relationships. Traversing end-to-end is a 6-hop path query — performed in real time at 5,000+ requests per hour.


2. Inherit Pre-existing Domain Models — Don’t Reinvent Them

All three projects benefited from the existence of a canonical external data model for their domain. The discipline was to adopt those models rather than build proprietary alternatives.

For BT SRIMS, the relevant standard was the TM Forum’s network inventory model. This provided a mature, well-understood ontology for telco assets covering physical, logical, virtual, and service nodes. BT’s node types (Device, Device Interface, Logical Connection, Network Address, Service Configuration, and so on) mapped closely to TM Forum concepts whilst still having multiple proprietary variants. Yet another vocabulary is never needed for telecoms. Agreement on the expression of an existing vocabulary was required.

For NatureMetrics, the canonical model is biological taxonomy itself, the Linnaean hierarchy. Which is formalised in databases such as ITIS, GBIF’s Backbone Taxonomy, and the Catalogue of Life. Rather than building a custom species classification, the Database of Life imported and followed the established taxonomic ranks. This gave the graph instant interoperability with the broader biodiversity informatics ecosystem and meant that new species records could be placed correctly in the hierarchy without ontological judgement calls. It could also answer Bayesian queries about expected species from actual species observations.

For NHS England, the canonical model is SNOMED CT. A 350,000+ clinical concepts connected by 1.3 million IS_A relationships. SNOMED was designed as a graph. Importing the SNOMED release files (Concept and Relationship tables in tab-separated format) and representing them natively as Neo4j nodes and edges meant that clinical hierarchy traversal (e.g “find every subtype of diabetes”) became a single variable-length path query rather than a recursive CTE against a materialised ancestors table.

SNOMED CT hierarchy traversal. Alice’s specific codes (Type 2 DM, CKD Stage 3) are matched by any query for their parent concepts via [:IS_A*0..]. The “DM with CKD” concept satisfies both the Diabetes and Chronic Kidney Disease branches simultaneously.

If your domain has an established ontology, use it as your graph’s backbone. You get correctness, interoperability, and a shared vocabulary with domain experts at no extra cost. The graph’s job is to connect instances to that ontology, not to replace it.


3. Extensibility with Simplified Nodes

Avoid using graph node labels for things that should be relationship properties and also avoid using too many relationship types for things that should be relationship properties. Neo4j performs best when the graph schema is lean. Node labels should be kept to a minimum in favour of relationship proeperties.

SNOMED defines a clinical meaning. It represents either a disorder, a procedure, a substance, or a finding. These are expressed as a property on the SnomedConcept node rather than as separate node labels (Disorder, Procedure, Substance). The hierarchy itself carries the semantic weight through IS_A edges. This means adding a new concept type does not require a schema change. It requires adding a node with the appropriate property and connecting it to the existing hierarchy.

For the NatureMetrics taxonomy, the eight ranks are properties on Taxon nodes, not eight separate label types. A query for “all amphibians detected in the Thames basin” traverses from the Class node for Amphibia down through Order, Family, Genus, and Species to individual detection events. The traversal path carries the semantics. The labels stay simple.


The Database of Life: eight taxonomic ranks as a node chain, with a Detection Event hanging off Species carrying location, date, and confidence. A query for “all amphibians in the Thames basin” traverses Class → Species → Detection → Location in a single expression.

The test to apply when defining a new node label is: does this label enable a fundamentally different type of query, or is it just a convenient filter that could be expressed as a WHERE clause on a property? If it’s the latter, it should be a property.

The corollary applies to relationships. Avoid creating relationship types that differ only by the value of a property. A [:PRESCRIBED {drug_class: 'ACE inhibitor'}] edge is better than a [:PRESCRIBED_ACE_INHIBITOR] relationship type, because the latter multiplies your relationship type count by the number of drug classes and makes queries that need to match across multiple classes unnecessarily complex.


4. Versioning, Write Locking, and Metadata Governance

Writing to production graph databases requires early architectural design. With Graph RAG it may be simpler to write from a single system rather than text chunking direct into RAG. But with a complex live network monitoring capability that uses graph specifically for its root cause analytics function then writing comes from multiple sources. In BT the SRIMS inventory management system eventually took social events from various social media feeds that mentioned network outages tagged with a location, BT and frequently expletives.

Metadata about nodes (creation date, data source, version, confidence score) should not live on the node itself if it varies per relationship or per observation. It belongs on the relationship or in a linked metadata node. A species detection in the NatureMetrics database has associated metadata (the eDNA sequencing run, the detection confidence, the sampling protocol) that is specific to that detection event, not to the species or the location. Storing it on the relationship or on a linked Detection node keeps the core ontological nodes clean and reusable.

Write locking matters most in high-throughput environments. SRIMS handled 5,000+ order progress requests per hour and 50,000 product availability checks per day. At that volume, concurrent writes against the same network resource nodes create contention. The approach taken was to treat the authoritative state of inventory nodes as write-locked by the system of record, with all mutations passing through a controlled update path. This is not a Neo4j-specific pattern. It is general data governance. But it is more important in graph databases because a bad writes (and phantom writes) can corrupt relationship structure rather than just a row value.

A practical implementation of this is to hold canonical metadata — the definition of a product type, the SNOMED release version, the taxonomic authority for a species name — in an external document management or metadata registry and reference it from graph nodes by ID rather than duplicating it. When the canonical definition changes (SNOMED releases twice a year; species taxonomies are revised continuously), the graph nodes that reference the external record don’t need to be updated — only the external record does. The graph remains a store of relationships and observations, not a duplicate of the reference data. Netflix’s Key Value Data Abstraction Layer is an implementation of external reference data store as an appendage of the graph database.


5. Supporting Ontology Change Without Breaking Consumers

Graph databases feel inherently flexible. Add a label or relationship type at any time. Schema evolution leads to query inconsistency, broken traversals, and data that means subtly different things depending on when it was written. The discipline is to design for change from the start rather than treating it as an operational problem to solve later.

The first rule is additive-only schema changes. Never redefine what an existing node label or relationship type means — only add new ones. If you modelled (Device)-[:CONNECTS_TO]->(Device) for physical links and later need to represent logical overlay connections differently, add [:LOGICALLY_CONNECTS_TO] rather than overloading the existing type. Old queries continue to work; new queries use the new type.

Make the ontology version explicit in the graph itself. A (:OntologyVersion {version: '2.1', effective: '2026-01-01'}) node, with relationships pointing to the node types and relationship types it defines, gives you a queryable record of what the schema meant at any point in time. For the NHS graph this was essential: SNOMED CT releases twice a year, and codes can be retired or reclassified between releases. Knowing which SNOMED release a concept node came from is not an audit nicety — it affects whether a traversal is clinically correct.

When a relationship needs to carry more meaning than a property can express, promote it to a node. In the NatureMetrics model, a detection starts as (Specimen)-[:DETECTED_AT]->(Location). If you later need to attach sequencing metadata, environmental context, and confidence intervals to that detection event, you promote it: (Specimen)-[:HAS_DETECTION]->(Detection)-[:AT_LOCATION]->(Location). The Detection node absorbs the new attributes without touching the Specimen or Location nodes. This pattern — reification — is the graph equivalent of adding a junction table, but far more natural in a property graph.

Finally, keep stable reference nodes structurally separate from volatile instance data. Taxonomic hierarchies, SNOMED concepts, and TM Forum node types change slowly and on external schedules. Detections, faults, and patient records change constantly. Keeping them separate means a SNOMED release update touches the reference subgraph without disturbing patient data, and you can version the reference layer independently of the instance layer.


6. Abstracting the Query Interface with GraphQL

Ontology changes are unavoidable in long-lived systems. In the BT SRIMS project, the telecoms network model evolved as new virtualisation layers were introduced between the logical and physical tiers. These changes that would have broken any consumer querying the graph directly. The answer is to interpose a stable API contract between consumers and the graph, and GraphQL is the natural fit.

GraphQL sits naturally over a graph database because both think in terms of types and relationships. The key principle is that the GraphQL schema defines the consumer-facing contract, and the ontology changes behind it without breaking downstream systems. A consumer querying circuit { status capacity logicalLayer { device } } doesn’t know or care whether the underlying Cypher traverses two hops or five, or whether a relationship type was renamed in a SRIMS upgrade.

For Neo4j, the @neo4j/graphql library generates Cypher automatically from a GraphQL schema. You annotate your types with @relationship directives and it handles traversal. When the underlying graph structure changes, you update the directive mapping — the consumer-facing type stays the same:

type Circuit {
id: ID!
status: String
capacity: Int
device: Device @relationship(type: "TERMINATES_AT", direction: OUT)
}

If TERMINATES_AT is later refactored to CONNECTS_TO_DEVICE in the graph, the change is in that one directive. Consumers are unaffected.

The deeper pattern is resolver composition for traversals likely to shift. In a telecoms inventory, a circuit exists simultaneously at multiple layers — service, logical, physical. When the ontology gains a new virtualisation layer between logical and physical, any traversal that crossed that boundary now has an extra hop. Rather than letting queries auto-traverse across layer boundaries, write explicit resolvers for those joins:

type LogicalCircuit {
id: ID!
physicalPath: [PhysicalSegment] # resolved explicitly, not auto-traversed
}

The physicalPath resolver encapsulates the traversal. When the ontology changes, you update that one resolver. Every consumer querying physicalPath gets the correct result without knowing the path changed. Anything likely to change in the ontology should be behind a named resolver, not auto-traversed. That is where you spend your abstraction budget.

For large organisations where downstream consumers cannot cut over simultaneously — common in telecoms, where OSS and BSS systems may be on separate release cycles — run versioned schemas in parallel. A v1 and v2 GraphQL schema can sit over the same Neo4j instance, with v2 resolvers traversing the new ontology and v1 resolvers using compatibility shims or legacy traversals. Consumers migrate at their own pace. This is the pattern that makes ontology migrations survivable at enterprise scale.

Where the ontology spans multiple domains owned by different teams (service layer, physical layer, customer domain) thenn Apollo Federation allows the GraphQL schema to be split into subgraphs. Each team owns their type definitions and resolvers independently. The gateway stitches them at query time. A change to how physical racks are modelled does not require redeploying the service layer schema.

GraphQL abstraction does not protect against breaking changes to the data itself. If a node type is removed from the ontology entirely and a resolver depends on it, the resolver must handle that. And if the GraphQL schema is designed as a thin pass-through of the graph structure rather than around consumer use cases, you have moved the coupling one layer up rather than eliminated it. The schema must reflect how consumers think about the domain, not how the ontology models it.


7. Design for Query Performance from Day One

Query performance in graph databases is not something you tune retrospectively. The decisions that determine whether a traversal takes milliseconds or minutes are made when you choose your node labels, your relationship types, your property indexes, and your traversal patterns.

SRIMS was a fault analytics and service provisioning system. Response time was contractual — product availability checks had strict SLAs, and the system handled peak concurrency of 1,500 simultaneous requests. The thousand-fold performance improvement over the previous relational system came not just from switching databases but from designing the graph specifically around the query patterns that mattered.

The key practice is to identify your critical query paths before you finalise the schema. For SRIMS, the critical queries were end-to-end service path tracing (from customer service down through logical circuits to physical infrastructure), resource reservation, and capacity availability checking. The ontology was structured so that these traversals followed short, well-indexed relationship paths rather than requiring full graph scans.

For the SNOMED model, the critical query pattern is variable-length IS_A traversal. The performance characteristic of [:IS_A*0..] in Neo4j depends on the depth and branching factor of the hierarchy. The SNOMED concept hierarchy has a maximum depth of around 15 hops and an average of 4-5 hops from any leaf concept to a root. These bounds make variable-length traversal tractable, but the IS_A relationship must be indexed correctly and the query should be anchored at the most specific concept possible to avoid traversing the full 350,000-node hierarchy.

Index your entry points. In all three graphs, the performance-critical practice was ensuring that the nodes from which traversals started — patient nodes by ID, species by GBIF code, network devices by equipment identifier — had composite indexes on their lookup properties. A traversal that starts with a full label scan will be slow regardless of how efficiently the rest of the path is structured.

This applies equally in Gremlin. In TinkerPop compatible databases the equivalent of an entry-point index is a composite or mixed index defined at the graph management layer. A Gremlin traversal anchored on an unindexed property — g.V().has('Patient', 'id', 'P001') — will perform a full vertex scan unless the id property is indexed. The traversal language differs from Cypher, but the design principle is identical: identify your traversal entry points at schema design time and index them before you go to production.


8. Design for Growth — The Map of Life Principle

The NatureMetrics Database of Life had an unrealised potentialt to become a complete Map of Life structure, a living graph connecting every species detection, every known range, every taxonomic update, and every environmental variable. It didn’t reach that scale, but the architecture would have supported it. The ontology was designed to extend, not to constrain. This is a discipline worth building into every graph ontology from the start. Design the schema so that the graph can grow in all directions without restructuring.

Keep the core node types stable and additive. A Taxon node should be able to gain new relationships (to location, to detection event, to trait observation, to conservation status) without its label or its existing relationships changing. In the NHS graph, encoding a medication class as a node label rather than a property would have made it difficult to extend the model when new drug classes were added or classification schemes changed. The property approach means new drug classes are new data, not schema changes.


9. The Relationship Is the Logic

In a graph ontology, business logic lives in the relationship structure, not in node properties or application code. In BT SRIMS, whether a circuit is available for a new service is determined by traversing the logical and physical layers and checking reservation relationships. The topology defines is the answer.

In the SNOMED graph, whether ibuprofen is contraindicated for a patient with CKD stage 3 is determined by traversing the CONTRAINDICATED_WITH edge from ibuprofen to the CKD concept, then checking whether the patient’s condition is connected to that concept by IS_A. There is no lookup table, no if/else logic, no hardcoded SNOMED code list in the application. The rule is expressed as a graph pattern.

This is the deepest advantage of graph ontologies over relational schemas. Domain semantics can be encoded in the structure of the data rather than in the code that queries it. When the business logic changes (a new contraindication is discovered, a network topology changes, a species is reclassified) you add or modify relationships in the graph. The queries that traverse those relationships adapt automatically.


tldr, best practice design:

  • Draw the logical ontology before you open the database.
  • Inherit canonical domain models rather than inventing new ones.
  • Keep node labels and relationship types lean. Design metadata and versioning into the schema, not as afterthoughts.
  • Plan for ontology change with additive-only evolution, reification, and versioned reference layers. Abstract consumer queries behind a stable GraphQL contract so the ontology can move independently of downstream systems.
  • Identify your critical query paths and design the schema around them.
  • Build for extension by keeping core node types stable and additive. Put business logic in the relationship structure, not in application code.
  • Treat graph modelling as a semantic design problem first and a technical implementation problem second. The database is the easy part. The ontology is the work.

Leave a Reply