Home / Blog / ArchiMate & EA Practice

Architecture Linting: The Quality Checks Most EA Repositories Skip

Ask a room of developers what a linter does and you'll get a fast, confident answer: it flags code that isn't wrong, but isn't quite right either — a variable that shadows another, an import nobody uses anymore, a function that's grown past the point anyone can hold it in their head in one read. Ask a room of architects the same question about their models, and most will pause, because the category barely exists in the tools they use. A model either fails a governance rule or it doesn't. Everything short of that — the element someone imported once and never connected to anything, the service named three different ways across four diagrams, the description field that's been blank since the model was created — just sits there. Nobody's job is to notice it, and no tool is watching for it.

A distinction software engineers already have

Software engineering settled this question decades ago, mostly by accident. A compiler error stops the build: the code is syntactically or semantically broken, and nothing runs until it's fixed. A linter is a separate, softer layer sitting next to the compiler — it doesn't stop anything, but it flags patterns that correlate with bugs, confusion, or maintenance pain later. An unused variable won't crash a program. A function with fifteen parameters will compile cleanly. Neither is "wrong" the way a missing semicolon or a type mismatch is wrong. Both are worth a second look, which is exactly why linters exist as a distinct category of tool rather than being folded into the compiler itself.

Architecture models already have an equivalent to the compiler error, even if nobody calls it that: a deterministic governance rule. "Every Tier-1 application must have an owner." "Public-facing services must sit behind the approved API gateway." "No element may belong to two conflicting security zones." These are pass or fail. A model either satisfies them or it doesn't, and when it doesn't, that is a real problem someone has to fix before the model — or whatever depends on it, like a compliance sign-off — can be considered sound.

What most EA repositories don't have is the other layer. Nothing plays the role a linter plays for code: a pass over the model that doesn't block anything, doesn't require a rule to have been broken in the strict sense, but surfaces the accumulating small stuff that erodes a model's usability over time. That is the gap architecture linting fills, and it is worth being precise about what belongs in it and what does not.

The line that matters: a governance rule says "this model is not compliant until this is fixed." A lint check says "this is probably not what you meant, or it's going to cause someone a headache later — take a look." Confusing the two either makes governance too noisy to trust, or lets real smell accumulate under the excuse that nothing technically failed.

What actually gets checked

Once you start looking for architecture smells rather than architecture violations, there is a fairly consistent list — most of it obvious in hindsight, almost none of it enforced anywhere by default in the EA tools most teams already use. Mooodels runs a set of these checks automatically, and the shape of the list is worth walking through, because each item maps to a specific, recognizable failure mode teams already live with.

CheckSeverityWhy it matters
Orphaned / unused elementsWarningNothing references it — likely dead weight or a forgotten import
Unnamed relationshipsInfoA connection exists but its nature is undocumented
Duplicate elementsWarningSame real-world thing modelled twice under slightly different names
Invalid relationship typesErrorThe relationship isn't valid for the profile in use, e.g. ArchiMate
Inconsistent capitalizationInfo"customer-api" vs "Customer API" vs "CustomerAPI" fragments search
Missing descriptionsWarningThe model is unreadable to anyone but the person who built it
Missing ownersWarningNo one to ask when the element needs a decision made about it
Circular dependenciesWarningA depends on B depends on A — usually unintentional, worth a look
Excessive couplingInfoOne element with an outlier number of relationships — a hidden bottleneck
Unresolved referencesErrorA relationship points at an element that no longer exists
Inconsistent technology namingInfo"PostgreSQL", "Postgres", "postgres 14" fragmenting one fact
Redundant servicesInfoTwo services that appear to do the same thing, possibly built twice

The severity column is not decorative. An invalid relationship type or an unresolved reference is close to a compiler error — the model is structurally broken in a way that will cause real failures downstream, in an export, in a rule evaluation, in an integration that reads the model programmatically. Most of the rest sit lower: they won't break anything today, but they are the kind of thing that, left alone, makes a model progressively less trustworthy and less usable. That is the same split software linters draw between an error-level rule — a genuine bug pattern — and a style or convention warning, and it matters for the same reason. If everything is flagged at the same severity, people stop reading the flags.

Four checks worth walking through in detail

Orphaned elements: "someone imported this once"

Every model that has been imported from somewhere — a spreadsheet, an older tool's export, a CMDB extract — accumulates orphans. An element gets pulled in because it was in the source data, nobody gets around to connecting it to anything, and it sits there indefinitely: present in the model, absent from every view that would give it context, invisible to anyone browsing rather than searching by exact name.

The problem compounds because orphaned elements do not look urgent. Nothing is failing. No rule is violated — an element with no relationships is not inherently non-compliant with anything. But six months later, when someone runs an impact analysis and the answer comes back "nothing depends on this," they have no way of knowing whether that is genuinely true or an artifact of an import that nobody ever finished connecting. An orphaned element does not sit there neutrally; it actively undermines the credibility of every query run against the model, because now every "zero results" answer carries a mental asterisk — zero results, or nobody ever connected it?

A check for this is almost embarrassingly simple to write — any element with no incoming or outgoing relationships, flagged — and almost nobody runs it, because there is no natural moment in a manual review process where someone thinks to ask the question. It is not wrong enough to escalate. It is just quietly there, and it stays there until a lint pass names it.

Missing descriptions: the model that only its author can read

A model with populated elements, correct relationships, and clean naming can still be functionally useless to everyone except the person who built it, if none of it is described. "Payment Gateway" tells you what something is called. It does not tell you what it does, why it exists, who to talk to before changing it, or what breaks if it goes down. That context either lives in the model as a description, or it lives in one person's memory — and one person's memory is not an architecture asset, it is a single point of failure with a job title.

This is the check that most exposes the gap between "the model exists" and "the model is usable by the organization." A repository can pass every structural rule — every relationship valid, every reference resolved — and still be a graph of correctly typed boxes that nobody but its author can interpret. Flagging elements above a certain importance threshold, say Tier-1 applications or anything referenced by a compliance-relevant view, that carry no description turns an invisible gap into a visible, assignable task, instead of a fact that only surfaces the day the original author leaves and someone else has to guess.

Inconsistent naming: the thing that breaks search and duplicate detection together

"customer-api", "Customer API", "CustomerAPI", "Customer_API" — four strings, one underlying service, in a model built by more than one person across more than one import cycle. Individually, none of these is wrong. Ask a governance rule whether an element has a name, and all four pass. But naming inconsistency is not really a naming problem — it is a search and deduplication problem wearing a naming costume.

Search relies on names matching, roughly, what people type. A search for "customer api" that misses "CustomerAPI" because of a formatting difference does not just annoy the person searching — it produces a false negative that looks exactly like a true negative, and nobody double-checks a search that returns zero results. Duplicate detection has the same weakness in reverse: an automated pass looking for two elements that are probably the same real-world thing is far less reliable if it has to guess through four capitalization and separator conventions rather than compare normalized names. Inconsistent naming, left unchecked, degrades every downstream operation that assumes names are roughly canonical, which is most of them.

Unresolved references: the failure that waits for export day

A relationship pointing at an element that no longer exists is the closest thing on this list to a genuine error rather than a smell, and it is worth including anyway, because of when it tends to get noticed. An element gets deleted; something else still references its old identifier; the model looks fine in the editor, because the view simply does not render a relationship it cannot resolve, so there is no visual gap, no red flag, nothing that draws attention during normal browsing.

The failure shows up later, somewhere else. An export to another tool chokes on a dangling reference. A rule evaluation silently skips a relationship it cannot resolve and returns a false pass. An integration that reads the model programmatically throws an exception nobody was expecting on a Tuesday afternoon. Unresolved references are a case where the cost of the problem and the moment of its discovery are pushed apart in time, which is exactly the pattern that makes continuous checking valuable: a check that runs at the moment of deletion catches it for free, while a check that only runs when someone happens to attempt an export catches it at the worst possible moment, with the least context about why it happened.

The rest of the list, briefly

The remaining checks follow the same logic at smaller scale. Duplicate elements are the naming problem's structural cousin — two elements that are probably the same thing, sitting in the model as two things, quietly splitting whatever relationships and history should belong to one. Circular dependencies, A depends on B depends on C depends back on A, are usually accidental and worth a look, though not always wrong — some feedback loops are real and intentional, which is exactly why this belongs in the lint category rather than the governance category: it needs a human glance, not an automatic rejection. Excessive coupling, one element with a strikingly high relationship count relative to its neighbors, often marks a hidden bottleneck or a service that has quietly become load-bearing for far more of the landscape than anyone planned. Redundant services flag the case where two elements look like they solve the same problem, which is worth a conversation even when both are individually well modelled. Unnamed relationships and invalid relationship types are closer to hygiene: a connection with no label tells you two things are related without saying how, and an invalid type usually means someone picked the nearest available option in a dropdown rather than the correct one for the profile in use.

None of these, on their own, are dramatic. That is the point. A linter's value was never about catching the one catastrophic bug — that is the compiler's job, or the governance rule's job. It is about catching the hundred small things that individually do not matter and collectively make a codebase, or a model, harder to trust than it should be.

Two feedback loops compared: linting on every save versus a review that happens twice a year Lint on every save each dot: one save — drift caught within seconds Manual review, twice a year review 1 review 2 unmanaged drift accumulates for months between the two dots on the calendar
Continuous linting turns drift into a series of small, cheap corrections. A biannual review turns the same drift into one large, unpleasant one.

Why "on every save" is a different thing than a review that happens twice a year

It is tempting to treat automatic linting as a nice-to-have version of the same manual model-quality review a team already does — a "quarterly architecture health check," a workshop where someone walks the repository looking for problems. The mechanics look similar: both produce a list of things to fix. The outcomes are not similar at all, for the same reason lint-on-save and an annual code audit produce very different codebases even when they are, on paper, checking for the same categories of problem.

The difference is the length of the feedback loop, and length changes everything about what gets fixed and how much it costs to fix it. A naming inconsistency caught the moment it is introduced is a thirty-second correction, made by the person who just typed the inconsistent name, while the reason for the choice is still in their head. The same inconsistency caught eight months later, after eleven more elements have been named to match it because it had quietly become "how we do it here," is not a thirty-second correction anymore. It is a rename campaign, a conversation about which convention is actually correct, and a risk that whoever does the cleanup gets it wrong because they were not the one who made the original decision.

Manual reviews also have a structural bias toward the big and the visible. Put a person in a room for two hours, twice a year, and ask them to find problems in a model with hundreds of elements, and they will — reasonably — spend that time on what looks urgent: a relationship that seems obviously wrong, a rule that seems obviously violated. An unnamed relationship or a mildly inconsistent capitalization convention does not compete well for attention in that setting. It is not that reviewers are careless; a scarce two hours gets rationed toward whatever feels most consequential in the room, and lint-level smell rarely does, even though it is exactly what quietly degrades search, deduplication, and trust between now and the next review.

There is also a population effect worth noticing. A model checked continuously never accumulates a large backlog of small problems, because each one gets caught close to when it was introduced. A model checked twice a year accumulates for months, and the review that finally happens is not evaluating "did we introduce a problem today," it is evaluating "how much has drifted since anyone last looked" — a fundamentally larger, more discouraging task. Teams that inherit that kind of backlog tend to spend the session on triage instead of architecture, or quietly lower the bar for what counts as worth fixing. Neither is a failure of the people involved; it is what happens when the feedback loop is too long for the volume of small decisions a model actually accumulates.

Mooodels runs its lint pass on every save and every commit, not as a gate that blocks anything, but as a background pass that surfaces findings in the same place you already look for everything else about the model. The goal is not a clean report once a year. It is a model that never has the chance to accumulate six months of unnoticed drift in the first place.

A worked example

Take a claims-processing domain inherited from an older repository — forty-odd elements, imported in one pass a few years ago and extended piecemeal since by different people. Nobody has done a dedicated quality pass on it, because nothing about it has ever technically failed a rule. Running a lint pass over it produces something like this:

Architecture Lint — 42 elements, 61 relationships scanned

[error]   Unresolved reference: relationship #142 → "old-claims-db" (element not found)
[warning] Orphaned element: "Legacy Rating Engine" — no relationships
[warning] Missing description: "Claims Intake Service" (Tier-1, referenced by 3 views)
[warning] Missing owner: "Claims Intake Service" (Tier-1)
[info]    Inconsistent naming: "claims-service", "Claims Service", "ClaimsSvc" — possible duplicates
[info]    Inconsistent technology naming: "PostgreSQL", "Postgres", "postgres 14" (3 elements)
[info]    Excessive coupling: "Policy Master" — 27 relationships (domain average: 4)
[warning] Circular dependency: Claims Intake Service → Rating Engine → Policy Master → Claims Intake Service

7 findings — 1 error, 4 warnings, 2 info

None of this is a surprise once it is written down, and that is exactly the point — every one of these was sitting in the model already, unflagged, because nothing about the model had technically broken. The unresolved reference is the one item that needs fixing immediately, because it will actively cause a failure the next time this domain is exported or evaluated against a rule that touches that relationship. The three naming variants of "claims-service" are worth ten minutes deciding on a canonical form and consolidating, because every day they stay split is a day search and duplicate detection are quietly unreliable for this part of the model. The circular dependency might be completely intentional — claims processing genuinely can have legitimate feedback between intake, rating, and policy lookups — but it is exactly the kind of thing a human should glance at and confirm rather than assume, which is why it is a warning and not an automatic rejection.

What makes this example ordinary rather than exceptional is that none of it required anyone to go looking. It came out of a pass that runs the same way on every model, every time something changes, and it turned forty minutes of half-remembered concerns — "I think Policy Master has a lot of connections," "wasn't there an old rating engine we never cleaned up" — into a specific, actionable list with severities attached, the kind of list a person can actually work through instead of one they have to first go excavate.

Linting is not governance, and should not pretend to be

It is worth resisting the temptation to turn every lint finding into a blocking rule, even once the checks exist and prove useful. Governance rules earn the right to block because they represent a real, agreed constraint — a Tier-1 application really does need an owner before it can be considered compliant, full stop. Most lint findings do not carry that kind of certainty: a circular dependency might be a mistake or might be intentional; an element with unusually high coupling might be a bottleneck worth redesigning, or just a genuinely central piece of infrastructure. Treating every one of these as a hard failure produces the architecture equivalent of a linter configured to error on every style preference — technically thorough, and ignored within a month because nobody can get anything past it.

The useful middle ground is the one software linting already settled on: severity levels that separate "this will break something" from "this is worth a look," surfaced continuously rather than saved up for a big reveal, with the option to suppress a specific finding once a human has genuinely decided it is fine as is. A model that flags everything at the same volume trains people to stop reading the flags — which defeats the purpose more thoroughly than not checking at all, since an unchecked model at least does not create false confidence that someone is watching.

What this changes about who does the cleanup work

There is a quieter benefit to continuous checks: who ends up fixing things, and with how much context. A finding surfaced close to when the issue was introduced usually lands in front of the person who introduced it — the one who just named the element, just imported the batch — while the reasoning is still fresh, and it gets corrected in seconds. A finding that only surfaces at a biannual review lands on whoever is running that review, who was almost certainly not the person who made the original decision and now has to reconstruct the reasoning, or the absence of it, before fixing anything safely. Continuous linting keeps small decisions attached to the people who made them; periodic review detaches them, and detached small decisions are exactly what turns into an hour of archaeology instead of a thirty-second fix.

A Git-friendly, deterministic model representation compounds this. If saving the model produces a stable, diffable serialization, a lint finding tied to a specific commit can be reviewed the way a code change is reviewed — see exactly what introduced the orphaned element or the naming inconsistency, in context, rather than staring at a static model and wondering when the drift actually happened.

None of this replaces governance, and it is not supposed to. A model can be fully lint-clean and still violate a real rule — pass every smell check and still put a public-facing service outside the approved gateway, which is a compliance failure, not a smell. The two layers answer different questions: governance asks "is this model allowed to be this way," linting asks "is this model going to be pleasant and reliable to work with." A model that only ever answers the first question can still be technically compliant and functionally miserable to use — full of orphaned elements, inconsistent names, and descriptions nobody wrote, none of which violates anything, all of which makes the model progressively less trustworthy every time someone tries to rely on it. Mooodels treats both as first-class, running deterministic governance rules and architecture linting side by side, on the same model, on every save — because a model that is merely compliant and a model that is actually usable are not the same achievement, and only one of them is checked by most EA repositories today.

See the model this article describes, working in a real editor.

Try the live demo