Skip to main content
EventCatalog/OCTOBER 18, 2026

Introducing Architecture Fitness Functions for EventCatalog

1 MINUTES READ
Summary

Write your architecture rules as TypeScript functions that read your catalog through the EventCatalog SDK. Keep them next to the domains and services they're about, run them in CI, and see exactly which resources break them.

Your team agrees that services in the Ordering domain should never read from the Payments database. If Ordering needs to know about a payment, it listens for an event or calls the Payments API. You write it down, maybe in an ADR, and everyone nods.

Six months later, someone adds the Payments data store to the Order Service's readsFrom. It was the quickest way to get a feature out. The catalog now documents the dependency perfectly. It doesn't tell anyone that it breaks a decision you made.

That's the gap I keep seeing. Teams spend real time documenting their architecture and agreeing on how it should look, but nothing checks that it still does.

Architecture fitness functions close that gap. The idea comes from Building Evolutionary Architectures by Neal Ford, Rebecca Parsons and Patrick Kua: a fitness function is an automated check that tells you whether your architecture still meets a goal you set for it. Your architectural decisions stop being something people have to remember, and become something that passes or fails.

So we built them into EventCatalog. You can now write architecture fitness functions for your catalog: rules about your architecture, written in TypeScript, that run against your catalog and tell you which resources break them.

How fitness functions work in EventCatalog​

In EventCatalog, a fitness function is a TypeScript function that reads your catalog and returns a list of violations. An empty list means the rule passed.

If you've written a unit test, you already know the shape. The difference is what's being checked. A test checks how your application behaves. A fitness function checks the architecture your catalog describes: your domains, systems, services, messages, data stores, owners, and how they relate to each other.

You write the rules. EventCatalog finds them, runs them and reports the results.

Write your first fitness function​

Here's a rule that every event must have an owner. Save it as fitness/event-ownership.fitness.ts in your catalog:

fitness/event-ownership.fitness.ts
import { defineFitnessFunction } from '@eventcatalog/fitness';

export default defineFitnessFunction({
// A unique id, and the name you'll see in the results
id: 'event-ownership',
name: 'Every event has an owner',
// 'error' fails the check when violations are found, 'warning' only reports them
severity: 'error',

// EventCatalog calls run() with read-only access to your whole catalog
async run({ catalog }) {
// The latest version of every event, using the same method as the EventCatalog SDK
const events = await catalog.getEvents({ latestOnly: true });

return events
// Keep the events that don't list any owners...
.filter((event) => !event.owners?.length)
// ...and turn each one into a violation. An empty array means the check passed
.map((event) => ({
message: `${event.name} has no owners`,
// The resource the violation is about, so you can find it in your catalog
resources: [{ type: 'event', id: event.id, version: event.version }],
// Tells whoever reads the result how to fix it
suggestion: 'Add a team or user to the owners field in the frontmatter',
}));
},
});

run is given a catalog, which has the same read methods as the EventCatalog SDK: getEvents, getService, getDomain, getGraph and the rest. There's no new query language to learn. If you've used the SDK, you can write a fitness function.

Each violation has a message, the resources it's about, and optionally a suggestion for how to fix it. severity decides what happens when violations are found: error fails the check, warning reports them without failing it.

Run it from the root of your catalog:

npx eventcatalog fitness check
EventCatalog Fitness

✗ Every event has an owner 1 violation
• Order Amended has no owners
Resources: order-amended (event)
Suggestion: Add a team or user to the owners field in the frontmatter

1 fitness function evaluated

0 passed
1 failed

Report written to .eventcatalog/fitness/results.json

Define rules where your architecture lives​

Ownership is a rule for the whole catalog, so it lives in the fitness/ folder at the root. But a lot of architectural decisions belong to one team. The Ordering team knows where its boundaries are better than anyone.

So you can also put fitness functions next to the resource they're about. Add a fitness/ folder to a domain, system, service, message or data store, and the function is given that resource when it runs:

fitness/
event-ownership.fitness.ts # the platform team's standards

domains/
Ordering/
index.mdx
fitness/
domain-boundaries.fitness.ts # given the Ordering domain
Payments/
index.mdx
fitness/
payment-contracts.fitness.ts # given the Payments domain

Here's the rule from the start of this post, kept with the Ordering domain:

domains/Ordering/fitness/domain-boundaries.fitness.ts
import { defineFitnessFunction } from '@eventcatalog/fitness';

export default defineFitnessFunction({
id: 'ordering-domain-boundaries',
name: 'Ordering services only use Ordering data stores',

// This file lives in domains/Ordering/fitness/, so `resource` is the Ordering domain
async run({ resource, catalog }) {
// Checking the type tells TypeScript that `resource` is a domain
if (resource?.type !== 'domain') return [];

// Collect the data stores and services that belong to Ordering.
// They're documented on the domain's systems, so read each system from the catalog.
const ownedStores = new Set<string>();
const services = [...(resource.services ?? [])];
for (const pointer of resource.systems ?? []) {
const system = await catalog.getSystem(pointer.id, pointer.version);
system?.containers?.forEach((container) => ownedStores.add(container.id));
services.push(...(system?.services ?? []));
}

const violations = [];
for (const pointer of services) {
// Read the full service to get its readsFrom and writesTo relationships
const service = await catalog.getService(pointer.id, pointer.version);
if (!service) continue;

// Every data store the service reads from or writes to...
for (const store of [...(service.readsFrom ?? []), ...(service.writesTo ?? [])]) {
// ...is fine if Ordering owns it...
if (ownedStores.has(store.id)) continue;
// ...and a violation if it belongs to another domain.
// Listing both resources shows exactly which dependency crossed the boundary.
violations.push({
message: `${service.name} uses ${store.id}, which belongs to another domain`,
resources: [
{ type: 'service', id: service.id, version: service.version },
{ type: 'container', id: store.id },
],
suggestion: 'Consume an event or call an API owned by the other domain instead',
});
}
}
return violations;
},
});

The function starts from the Ordering domain, then uses catalog to follow it to its systems, their services, and the data stores those services read from and write to. Where a function lives decides where it starts, not how far it can look. Every function can read the whole catalog.

This matters when ownership is spread across teams. A platform team keeps the standards everyone follows in the root folder. Each domain team writes the rules for its own boundaries, reviews them in the same pull requests as the rest of its documentation, and doesn't need anyone's permission to add one. Read more about global and resource fitness functions, including how versioned resources work.

Run fitness functions in CI​

eventcatalog fitness check exits with 1 when an error function finds a violation, so it fails your pipeline. Add it to the workflow that already runs on changes to your catalog:

.github/workflows/fitness.yml
jobs:
fitness:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx eventcatalog fitness check

Now the pull request that adds payment-database to the Order Service fails before it's merged:

EventCatalog Fitness

✗ ordering / Ordering services only use Ordering data stores 1 violation
• Order Service uses payment-database, which belongs to another domain
Resources: order-service (service), payment-database (container)
Suggestion: Consume an event or call an API owned by the other domain instead
✓ Every event has an owner Passed

2 fitness functions evaluated

1 passed
1 failed

Report written to .eventcatalog/fitness/results.json

The conversation happens in the review, when it's cheap to change course, instead of six months later when something else depends on it.

Fitness functions check what your catalog documents. They don't read your application code or watch production traffic. If your catalog is kept up to date from your code, with a generator or in the same pull requests, the checks follow your architecture as it changes.

When you're introducing a rule to a catalog that already breaks it, start it as a warning. Teams see the violations without their builds failing, and you can make it an error once they're fixed. If a function throws, it's reported as an error rather than a pass, so a broken rule can't go unnoticed.

See running fitness functions in CI for every exit code and a GitLab example.

Violations point at your architecture​

A failed rule on its own isn't much help. "Domain boundaries: failed" leaves someone digging through the catalog to work out what happened.

So every violation names the resources it's about. EventCatalog looks each one up in your catalog and records whether it exists and which version was checked. In the example above, that's order-service 1.0.0 and payment-database 1.0.0, even though the function never gave the data store's version. If a function points at something that isn't there, the result says not found in the catalog, which usually means a typo in the rule.

Every run also writes a JSON report to .eventcatalog/fitness/results.json: each function's status, its violations, the resources they point at, the resource the function sits alongside, and the git commit the catalog was checked at. Keep it as a build artifact, or feed it into your own dashboards. The report reference describes every field.

What can you build with fitness functions?​

Anything you can work out from your catalog. Some ideas to start with, several of which have ready-to-use code in the examples:

  • Enforce domain boundaries. Services only use their own domain's data stores, or only receive messages from domains they're allowed to depend on.
  • Detect circular dependencies. Work out which services depend on which from what they send and receive, and flag any loop.
  • Limit coupling. Warn when a service receives more messages, or touches more data stores, than you're comfortable with.
  • Require ownership. Every event, service and data store has an owner, and that owner is a team that exists.
  • Validate documentation standards. Business-critical services have a repository link, an owner and a schema for every message they send.
  • Find deprecated dependencies. Flag any service that still sends or receives a message you've marked as deprecated.
  • Enforce contract conventions. Event names are past tense, every event has a schema, and every channel has an address.

What you can check depends on what your catalog records. A rule about data store access needs readsFrom and writesTo. A rule about criticality needs a badge or a custom property you agree on. The more of your architecture you document, the more you can check.

Get started​

Add a fitness/ folder to your catalog, write your first function, and run:

npx eventcatalog fitness check

Starting a new catalog? npx @eventcatalog/create-eventcatalog@latest now comes with two starter fitness functions, "Every message has an owner" and "Every event has a producer", and an npm run fitness script, so you can see them work before you write your own.

Create a fitness function walks you through your first rule in about five minutes, and Run in CI adds the check to every pull request.

Your architecture, your rules. I'd love to see the rules you write. Read the fitness function docs, share your rules in Discord, or open an issue on GitHub if there's something you want to check and can't.