A correction to the fifth post: enforcing the ArchiMate metamodel in SHACL works, but it costs a lot more than I let on. Theory… meet practice.
This post was going to be the sixth in the planned series on my ArchiMate Ontology. It was going to be SPARQL heavy, I was going to show you how to query your ArchiMate RDF database, how to run impact analysis, forward and reverse, how to do compliance scope, how to nail derived relationships and provenance… how I nailed it all with RDF, how I invented the silver bullet. After post 5, I took some time to build my workbench where I set out to test the whole thing with a real model, the ArchiSurance model. That’s when I found out reality. SHACL validation is expensive.
I found that out the way you usually find these things out, on a real model, at real size, a few weeks after telling everyone the metamodel was enforceable by construction. It is enforceable. I just hadn’t paid the bill yet. I presumed too much in post 5. I’m biting the bullet now. I’m biting the bullet hard.
Back in post 1 I admitted the validation shapes had only ever run against synthetic test cases, and that real models would find the edges. I finally got to the point where I could load The Open Group’s ArchiSurance case study: 361 elements and 600 hand-drawn relationships. I switched on validation and derivation together, and started using it the way I would use the real thing. The edges showed up fast.
Nobody puts this part on the box. RDF4J validates the changes in your transaction rather than the whole repository, which sounds reassuring until you read the next clause: it pulls in whatever pre-existing data it needs in order to judge those changes. How much it pulls in is decided entirely by how you wrote your shapes. A broad target or an unbound predicate quietly turns “validate my one new triple” into something closer to “walk the model.” On a toy model you never feel the difference. On ArchiSurance a single edit could take north of twenty seconds to commit, and more than once the repository fell over with an out-of-memory error and took my afternoon with it. (There is one operation that really does revalidate everything, and that’s reloading the shapes graph itself. At seventy-odd thousand triples it ran my heap out twice in a row.) My first instinct was to blame derivation, since materializing thousands of implied relationships sounds like the pricey part. That instinct was wrong, and it took a benchmark to show me how wrong.
What the testing surfaced
The worst offender was a shape I wrote myself. CrossDomainRestrictions was supposed to enforce ArchiMate’s B.4 cross-domain restrictions. It never did. It only ever compared the two endpoint domains of a relationship and ignored the bridging element in a derivation chain, which is the entire point of B.4, and which you can’t even see without provenance. So it was wrong on the semantics. It was also slow as hell, eleven to eighteen seconds of added validation on every write, all of it from an overscoped target and an unbound predicate. A wrong answer that was also expensive to compute. I had to pull it.
Then there were the self-inflicted quadratics. CompositionPartUniqueness did an unconstrained self-join, which on a single well-connected node produces an N-by-N cross product and hundreds of thousands of validation results out of one element. That is what shat out the out-of-memory crashes. The same lazy pattern was sitting in a sibling shape, JunctionRelationshipHomogeneity. Rewriting both as a GROUP BY / HAVING aggregate took them from N-squared back to linear. A third shape, UniqueConceptIdentifier, taught me something worse. I rewrote it the same way, GROUP BY and HAVING, on the reasonable theory that whatever fixes one quadratic fixes the next. It didn’t. The aggregate I gave it was uncorrelated, so instead of checking one concept’s identifier it re-scanned every identifier in the model on every single write. I hadn’t written the word quadratic this often since high school. Measured on its own it came in at fourteen seconds, which made it the most expensive shape in the whole Level 1 set, worse than either of the two that had crashed the repository. The fix was a correlated self-join, and it dropped to three tenths of a second. Same rewrite: a fix in one shape, a regression in another. None of that is clever. It’s the SHACL equivalent of a missing database index, except that the index you add out of habit can be the thing that slows you down.
A subtler one cost me a full day. A depth-limit shape meant to fire at six hops was firing at one, because I’d left its counting variables unconstrained. Worse, I’d set its severity to Warning on the assumption that a warning is advisory. In RDF4J’s ShaclSail, which is the engine GraphDB runs, a Warning still blocks the transaction. There is no “just warn me” unless you deactivate the shape. The same trap was hiding in a second shape I thought I’d switched off and never had. Read the semantics your engine implements, not the ones the word implies.
Then there’s the part I’m least proud of. SHACL has an advanced-features spec with sh:rule and sh:SPARQLRule, so I’d written derivation rules as CONSTRUCT bodies and filed them in the shapes graph, where they sat looking exactly like an inference engine. They never ran. RDF4J’s ShaclSail validates; it does not execute rules. Nothing in the stack complains about this, because nothing in the stack is obliged to: a rule it doesn’t run is just a triple it doesn’t care about. The actual derivation was being done by hand-written queries somewhere else entirely, which means that for months I had two derivation engines that were free to disagree with each other, and the authoritative-looking one was decorative.
The same graph had a second surprise waiting. I’d parked the relationship-strength values that the derivation ranking depends on in that shapes file too, because that’s where the rules were. RDF4J hides the shapes graph from ordinary SPARQL. Every strength lookup came back empty, every chain scored as neither structural nor dependent, and the filter dropped every candidate before it reached a single real relationship. The feature wasn’t failing at the edges, it was silently off for every model, and it queried clean, because an empty result is a perfectly good result. I eventually found the triples through the raw shapes-graph endpoint. If you put reference data in the shapes graph, no query you write will ever see it.
And here’s the one that changes how you work day to day. Validation scope is the whole repository, not your change. One lingering violation anywhere blocks every later write and every load, for everyone on that repo, until somebody clears it. Enforcement by construction is a strong guarantee. It’s also a single shared gate, and you feel that the first time a stray triple from last week locks you out of your own import.
Derivation wasn’t the problem
To find out whether derivation was really the cost, I took it out of the equation and tested it alone. Two throwaway repositories with no SHACL in them at all: one running GraphDB’s built-in ruleset, one running a custom .pie rule with the multi-variable join that real derivation needs. Both materializing two-hop chains, scaled from a thousand up to a hundred thousand. I deleted a single premise each time and watched the engine retract the inferred triple. It stayed flat at roughly a sixth of a second at every size, in both. The custom rule is the result that matters, because a custom join is the case you get warned about. Retraction is incremental. It does not re-evaluate the model, so it does not grow with the model. The scaling fear I’d carried about derivation was unfounded, and the twenty-four-second writes had been SHACL the whole time.
So the design changed. Valid derivations move out of SHACL and out of hand-written queries into a GraphDB reasoning ruleset that fires for the whole repository and retracts automatically when a premise disappears. The engine maintains truth so I stop hand-rolling it. Potential derivations, the ones that need a modeler’s judgment, get computed on demand and never stored, offered as suggestions rather than facts. That division is what I should have designed from the start, and it took watching the wrong version melt down to see it.
There’s a limit to this that I should name, because I ran into it about an hour after being pleased with myself. Forward chaining has no negation. ArchiMate’s restriction rules are shaped like “this derived relationship holds unless some other relationship is absent,” and you cannot say unless in a rule language that only knows how to add. So the positive derivations move to the ruleset and the restrictions can’t follow them there. They have to be evaluated somewhere that does have negation, which means SPARQL, which means choosing whether a restriction rejects a model outright, filters it at read time, or just flags something for a human to look at. I don’t have that answer yet. It’s the next thing I get to be wrong about.
There’s a cost to that decision I want to name too, because I’ve spent the whole series arguing for open standards. A GraphDB reasoning ruleset is engine-specific. It is not a W3C standard. The way I square it: the portable, standard form of the rules stays the source of truth, and the engine ruleset is a compilation target for the deployment I run. Standard where the meaning lives, specific where the workload is paid for.
What I’d tell myself six months ago
- Test on real, normal-sized data before you claim anything holds. Synthetic cases pass precisely because they never trip the costs that only appear at scale.
- Validation on write is validation of everything on write. Budget for it, keep every shape’s target as narrow as you can, and measure the marginal cost each shape adds to a commit.
- “Warning” is a lie until you check what your engine does with it. Mine blocked writes for a week while I assumed it was chatting.
- Prefer the native constraints.
sh:property,sh:in, cardinality, datatype: these are cheap and the engine handles them well. Nearly all of my cost was concentrated insh:sparql, and most of what I’d written as a query had no business being one. - Check that your engine runs the thing you wrote. A validation engine will hold your inference rules without executing them and without complaining, and a shapes graph will hold your reference data where no query can reach it.
- Measure before you blame the obvious suspect. I was certain derivation was the pig at the trough. It was the one thing in the room running fine.
Acknowledgments
None of this rethink is mine alone.
Thanks as well to Ron Deen, whose archimate-rdf-xml-adapter is what let me get ArchiSurance into RDF in the first place, so the model that broke my assumptions was a real one and not something I hand-fed.
The people at GraphWise in Sofia, Vladimir Alexiev and Viktor Senderov, took the time to look at what I was doing and pointed me at the reasoning ruleset instead of the wall I’d been headbutting. Viktor’s own scars with retraction are the reason I bothered to benchmark it rather than take the fear on faith. Vladimir also caught a metamodel mistake I’d carried through years of working with ArchiMate, and it’s going to leave a mark.
I still believe the thing can be built. I believe in it more now, because I’ve watched it break and I know why. But first, I got to right a lot of wrongs. Assuming I don’t find something else (but history and my hard head suggest I will), I should have an actual ending to this whole ArchiMate in RDF series.
The ontology is independent and is not an official publication of The Open Group. ArchiMate is a registered trademark of The Open Group.






Leave a comment