DEV Community

Cover image for Stop Writing Architecture Rules in Confluence
Lars Moelleken
Lars Moelleken

Posted on

Stop Writing Architecture Rules in Confluence

Implementation of the idea of the blog post:
https://github.com/voku/itp-context/

Use PHP attributes when the rule belongs to the code

Every serious codebase has rules.

Not formatting rules. Those are easy. Let PHP-CS-Fixer, PHP_CodeSniffer, Rector, PHPStan, and your IDE handle the boring whitespace police work.

I mean the rules that decide whether the system survives:

  • external APIs must stay behind adapters
  • domain code must not know HTTP clients
  • persistence must go through repositories
  • legacy compatibility must stay isolated
  • security validation must happen before data crosses a boundary

These rules exist in every real project.

The problem is where they live.

Usually, they live in someone’s head, an old ticket, a forgotten ADR, or a Confluence page that looks official and is already wrong.

That is documentation theater.

Everyone pretends the architecture is documented. Then someone changes the wrong class, bypasses the wrong boundary, and the team suddenly remembers:

“We agreed not to do it like that.”

Great.

Where?

Page 37 of the knowledge base.

Very useful. Very dead.

voku/itp-context solves a narrow, practical problem: attach architecture rules to code with PHP attributes, resolve them through a catalog, validate them, export them, and make them queryable for humans and coding agents.

Not magic.

Just better placement.


Detached rules rot

Documentation that lives away from the code has one fatal flaw:

It does not fail.

It does not fail builds.

It does not appear in diffs.

It does not stop a refactor.

It does not warn a coding agent before it crosses a boundary.

It waits somewhere else, slowly becoming archaeology.

That is fine for old pottery.

It is not fine when you need to know whether StripePaymentGateway is allowed to leak HTTP exceptions into your domain layer.

Architecture rules need two things:

  1. Proximity — the rule must be near the risky code.
  2. Identity — the rule must have a stable name tools can resolve.

Confluence gives you neither.

A PHP attribute pointing to an enum case gives you both.


The useful shape

The model is simple:

code symbol
    -> enum rule identifier
        -> catalog definition
            -> owner / rationale / refs / proof
Enter fullscreen mode Exit fullscreen mode

The code does not carry the essay.

The catalog does not float disconnected from implementation.

The ADR is still useful, but no longer alone.

The rule becomes connected project data.

That is the whole point.


Example: one boundary, one rule

Do not mix topics.

If the rule is about an external API boundary, keep the example about that boundary.

<?php

declare(strict_types=1);

namespace Acme\Context;

use ItpContext\Contract\RuleIdentifier;

enum ArchitectureRules implements RuleIdentifier
{
    case ExternalApiBoundary;
}
Enter fullscreen mode Exit fullscreen mode

That enum case is the stable architecture identity.

No strings.

No typo-friendly nonsense.

No #[Rule('external-api-boundary')] waiting to become external_api_boundary, ExternalApiBoundary, or ExternlApiBoundary.

Use symbols when the thing is part of the code model.

Strings are for text.

Architecture rules are not text.


The catalog explains the rule

The attribute should stay small.

The catalog carries the meaning.

<?php

declare(strict_types=1);

namespace Acme\Context;

use ItpContext\Enum\Tier;
use ItpContext\Model\RuleDef;

return [
    'ExternalApiBoundary' => new RuleDef(
        statement: 'Keep external API communication behind adapter boundaries.',
        tier: Tier::Required,
        owner: 'Team-Backend',
        rationale: 'Domain and application code must not depend on HTTP clients, transport errors, or provider-specific response formats.',
        verifiedBy: [
            'tests/Architecture/ExternalApiBoundaryTest.php',
        ],
        refs: [
            'docs/adr/external-api-boundaries.md',
        ],
    ),
];
Enter fullscreen mode Exit fullscreen mode

That is enough.

No essay in the class.

No architecture novel in a PHP attribute.

No Confluence page copy-pasted into source code like a crime scene.

The catalog says:

  • what the rule means
  • who owns it
  • why it exists
  • where to read more
  • how it is verified

That is operational context.

Not decoration.


The code marks the boundary

Now annotate the central symbol.

Not every implementation.

Not every method.

The boundary.

<?php

declare(strict_types=1);

namespace Acme\Payment;

use Acme\Context\ArchitectureRules;
use ItpContext\Attribute\Rule;

#[Rule(ArchitectureRules::ExternalApiBoundary)]
interface PaymentGateway
{
    public function authorize(PaymentRequest $request): PaymentResult;
}
Enter fullscreen mode Exit fullscreen mode

That is the right place.

A developer changing payment integration code should find this.

A coding agent changing payment integration code should find this.

A reviewer should see this and know:

  • provider-specific responses stay outside
  • HTTP exceptions do not leak into the domain
  • retries, logging, and error translation belong here
  • domain logic does not belong here

One rule.

One boundary.

One clear example.

No UI.

No translation.

No unrelated method-level noise.


Validation makes it real

Metadata without validation is decoration.

Decoration is where architecture goes to die wearing a nice badge.

If the enum contains ExternalApiBoundary but the catalog does not, the rule is incomplete.

If the catalog contains ExternalApiBoundary but the enum does not, the catalog is stale.

Both should fail.

vendor/bin/itp-context-validate 'Acme\Context\ArchitectureRules'
Enter fullscreen mode Exit fullscreen mode

Put it into CI.

Do not trust humans to keep metadata clean manually.

Humans forget.

CI does not care about your feelings.

Good.


Export makes it usable

Once the rule is attached to code, it can be exported.

vendor/bin/itp-context-export var/itp-context src --exclude=vendor --exclude=tests
Enter fullscreen mode Exit fullscreen mode

That export gives humans and coding agents something better than vibes.

Instead of writing this into an agent instruction:

Please follow our architecture.
Enter fullscreen mode Exit fullscreen mode

Write this:

Before changing payment integration code, inspect the exported itp-context entry for Acme\Payment\PaymentGateway. Respect the attached ExternalApiBoundary rule and check its referenced proof artifacts.
Enter fullscreen mode Exit fullscreen mode

That is better because it points to repository-owned context.

The prompt is not the architecture.

The repository is.

The prompt only tells the agent where to look.


Where annotations belong

Annotate central discovery points.

Not the whole tree.

Good places:

  • boundary interfaces
  • adapter base classes
  • application service roots
  • persistence contracts
  • legacy compatibility seams
  • security input boundaries

Bad places:

  • every concrete class
  • every method
  • every helper
  • every place where somebody felt nervous

If every class is annotated, nothing is important.

That is not architecture.

That is Confluence with PHP syntax.


What this is not

This is not a replacement for ADRs.

It is not a replacement for tests.

It is not a replacement for PHPStan.

It is not a replacement for code review.

It is not an AI framework.

It is not going to fix bad architecture by sprinkling attributes on top like holy water.

It does one useful thing:

It connects architecture rules to the code symbols where those rules matter.

That is enough.

Small tools are allowed to be useful without pretending to solve the universe.


Best practices

Use itp-context like this:

  • define broad rules
  • use enum cases as rule identifiers
  • keep attributes small
  • explain rules in the catalog
  • add owners and rationale
  • link ADRs, tests, and proof artifacts
  • validate enum/catalog integrity in CI
  • export compact context
  • annotate central symbols only
  • delete rules that do not change decisions

The last point matters.

A small rule catalog people trust beats a huge catalog everyone ignores.

Every time.


Summary

The problem is not Confluence.

The problem is architecture knowledge living somewhere else.

If a rule affects code, connect it to code.

voku/itp-context gives you a pragmatic way to do that:

  • PHP attributes attach rules to code symbols
  • enums give rules stable identities
  • catalogs hold meaning, ownership, rationale, refs, and proof
  • validation catches drift
  • exports make context usable for humans and coding agents
  • queries make architecture searchable instead of tribal

This is not about making PHP “AI-native.”

Good.

That phrase already smells like a conference booth.

The better goal is simpler:

Make architecture rules explicit enough that humans and tools can find them before they break them.

Because the real enemy was never the LLM.

The real enemy was architecture knowledge living somewhere else.

Usually in Confluence.

Beautiful, confident, and wrong.

Happy coding. 😊

Top comments (0)