← All plugins Plugin I — Architecture Enforcement
Architectural heresy ends here.
Architectural doctrine enforcement through composable lenses. Define your architecture as auditable rules, then let the Inquisition hold the codebase to them — commit by commit, or across the whole sanctum.
/puritan:covenant /puritan:inquisition /puritan:scriptorium How it works
Puritan's three skills work together as a continuous loop — plan, enforce, extend.
/puritan:covenant Decide patterns, generate config /puritan:inquisition Audit code against doctrines /puritan:scriptorium Author new or updated doctrines The skills
/puritan:covenant Architecture Planning
Covenant helps you choose architectural patterns before you build, or assess what
patterns your existing codebase is already using. Discover mode is the
fastest way to get started on an existing codebase — it reads directory structure only,
infers likely patterns, confirms with you, then writes
.architecture/config.yml ready for Inquisition.
Invocation modes
/puritan:covenant Full analysis — pattern recommendations + phased implementation roadmap /puritan:covenant discover Lightweight scan → detects patterns → generates .architecture/config.yml /puritan:covenant assess Gap analysis of current architecture against stated patterns /puritan:covenant roadmap Phased implementation plan (assumes patterns already chosen) /puritan:covenant risks Architecture risk analysis and mitigation strategies /puritan:inquisition Code Audit
Inquisition audits your codebase against the doctrines in
.architecture/config.yml. It dispatches parallel
subagents per doctrine and collates a structured violation report. Violations are
classified as error (blocks commit) or
warning (advisory). A large-codebase guard pauses
at 100+ files and lets you narrow scope before proceeding.
Invocation modes
/puritan:inquisition Changed files only (git diff against base branch) /puritan:inquisition full Entire codebase /puritan:inquisition interactive Full codebase, interactive — fix violations one by one /puritan:inquisition <doctrine> Changed files, single doctrine only error Correctness violation — blocks commit warning Quality concern — advisory /puritan:scriptorium Doctrine Authoring
Scriptorium writes new architecture doctrine files, or updates existing ones. It
researches the pattern from authoritative sources, structures the doctrine to the
required format, and places it in the doctrines/
directory ready for Inquisition to use. Each doctrine is a markdown file with a
structured violation catalog — typically 20–50 rules across 5–8 categories — each
with a concrete detection pattern.
Invoke when you want to codify an informal team rule, add a doctrine for a pattern not yet covered, update an existing doctrine, or convert an ADR into doctrine format.
Doctrine catalog
Expand any doctrine to see its full rule set — when to use it, why it matters, and the exact patterns Inquisition scans for.
DDD Pureness
This doctrine audits Domain-Driven Design implementation for isolation, consistency boundaries, and ubiquitous language compliance.
Foundational Works:
Aggregate Design Rules:
Value Objects and Entities:
Anti-Patterns:
Modern Practices (2020-2025):
| ID | Category | Rule | Severity | Scan For |
|---|---|---|---|---|
| DDD-001 | layer-boundary | Domain must not import from infrastructure | error | from <pkg>.infrastructure or import <pkg>.infrastructure in domain/ files |
| DDD-002 | layer-boundary | Domain must not import from API layer | error | from <pkg>.api or import <pkg>.api in domain/ files |
| DDD-003 | layer-boundary | Domain must not import from application layer | error | from <pkg>.application or import <pkg>.application in domain/ files |
| DDD-004 | layer-boundary | Domain must not import framework libraries | error | import sqlalchemy, fastapi, celery, redis, aiohttp, httpx, requests in domain/ files |
| DDD-005 | layer-boundary | Domain must not perform I/O | error | session.execute, await fetch, open(, publish(, HTTP calls, DB queries in domain/ files |
| DDD-006 | layer-boundary | Application must not import from API layer | warning | from <pkg>.api in application/ files |
| DDD-010 | aggregate-design | Aggregates must extend BaseAggregate | error | Classes in aggregates/ not inheriting BaseAggregate (excluding base.py) |
| DDD-011 | aggregate-design | Aggregates must not hold other aggregates by instance | error | Aggregate attributes typed as another aggregate class. References by ID (uuid.UUID) are correct |
| DDD-012 | aggregate-design | State changes must only happen via events | error | self.<field> = assignments outside _when_* handlers in aggregate files |
| DDD-013 | aggregate-design | Event handlers must be private _when_* methods | warning | Public methods mutating state from event data without following the _when_<snake_case> naming convention |
| DDD-014 | aggregate-design | Aggregates must implement snapshot methods | warning | Missing to_snapshot() or from_snapshot() on aggregate classes |
| DDD-015 | aggregate-design | Aggregate size should be manageable | warning | Aggregate class >500 LOC (Vernon: 300-400 LOC max), or >20 distinct event types, or >15 command methods |
| DDD-016 | aggregate-design | One aggregate per transaction | error | Service methods modifying multiple aggregates in one transaction (Vernon: consistency boundary rule) |
| DDD-017 | aggregate-design | Constructor must establish valid state | warning | Aggregate __init__ or create() methods with >5 parameters that perform no validation before emitting the creation event |
| DDD-020 | value-object | Value objects should use __slots__ | warning | Classes in value_objects/ without __slots__ |
| DDD-021 | value-object | Value objects must be immutable | error | Public setter methods, self.<field> = outside __init__, properties returning mutable references without copying |
| DDD-022 | value-object | Equality must be by value | warning | Value object classes without __eq__ (relying on identity comparison) |
| DDD-023 | value-object | Must provide serialization | warning | Missing to_dict() or from_dict() methods |
| DDD-024 | value-object | Arithmetic must return new instances | error | __add__/__sub__/etc. that mutate self instead of returning type(self)(...) |
| DDD-025 | value-object | Immutable VOs should be hashable | warning | Value objects with __eq__ but missing __hash__ (prevents use in sets/dict keys) |
| DDD-030 | entity | Entities must have identity | error | Classes in entities/ without an *_id attribute |
| DDD-031 | entity | Entities should use __slots__ | warning | Entity classes without __slots__ |
| DDD-032 | entity | Must provide serialization | warning | Missing to_dict() or from_dict() (needed for aggregate snapshots) |
| DDD-033 | entity | Entity equality must be by identity | warning | Entity __eq__ comparing attributes instead of just self.<id_field> == other.<id_field> |
| DDD-040 | event-design | Events must be immutable | error | Event classes without model_config = {"frozen": True} or Pydantic frozen inheritance |
| DDD-041 | event-design | Events must extend DomainEvent | error | Event classes in events/ not inheriting DomainEvent |
| DDD-042 | event-design | Events should not contain business logic | warning | Event classes with methods beyond serialization, properties, or model validators |
| DDD-043 | event-design | Event names must be past tense | warning | Event class names not past tense (e.g., CreateLoan instead of LoanCreated) |
| DDD-044 | event-design | Events must support schema evolution | warning | New event fields without default values, or handlers that don't use .get() for optional fields |
| DDD-045 | event-design | Events must be dispatched after state change | error | Aggregate methods that publish directly to an event bus instead of adding to uncommitted events list |
| DDD-050 | command-design | Commands must extend BaseCommand | error | Command classes in commands/ not inheriting BaseCommand |
| DDD-051 | command-design | Commands must use imperative naming | warning | Command names not imperative (e.g., LoanApproved instead of ApproveLoan) |
| DDD-052 | command-design | Validation must use Pydantic | warning | Manual if/raise validation in __init__ instead of Pydantic validators |
| DDD-053 | command-design | Commands must not contain business logic | warning | Commands with methods beyond Pydantic validators that perform domain calculations or call services |
| DDD-060 | domain-service | Domain services must be stateless | error | self.<field> = assignments in methods other than __init__ in domain services/ |
| DDD-061 | domain-service | Domain services must not perform I/O | error | Same as DDD-005 scoped to domain services/ |
| DDD-062 | domain-service | Domain services must not orchestrate | warning | Domain services that load aggregates from repositories/event stores, manage transactions, or coordinate multi-aggregate workflows (that belongs in application services) |
| DDD-070 | anti-pattern | Anemic domain model | warning | Aggregate with >5 properties but <3 business methods (Fowler: AnemicDomainModel) |
| DDD-071 | anti-pattern | Primitive obsession | warning | Aggregate fields typed as raw str/Decimal/int for domain concepts that warrant value objects (e.g., email addresses, currency amounts not using Money, status codes as raw strings) |
| DDD-072 | anti-pattern | Invariant enforcement outside aggregate | warning | Application/service layer performing business rule validation (if amount > limit: raise) that should live in the aggregate's command method |
| DDD-073 | anti-pattern | Missing anti-corruption layer | warning | Domain layer importing external SDK types or using external system terminology directly without a translation adapter |
| DDD-080 | naming | Domain objects should use business language | warning | Technical jargon in domain names (DBLoan, HttpPayment, CacheAccount, DataManager, EntityProcessor) |
| DDD-081 | naming | Avoid CRUD naming in domain | warning | Methods named create_record, update_row, delete_entry, insert_*, select_*. Prefer domain verbs: submit, approve, disburse, accrue |
| DDD-082 | naming | Avoid abbreviations in domain | warning | Abbreviated names (Mgr, Ctx, Proc, Svc) in domain layer. Full words improve readability |
| DDD-090 | temporal | Use utc_today() not date.today() | error | date.today() or datetime.now() without timezone.utc in domain/ files |
CQRS
This doctrine audits Command Query Responsibility Segregation implementation, ensuring proper separation of read and write models without unnecessary complexity.
Foundational Works:
Implementation Guidance:
Anti-Patterns and Pitfalls:
| ID | Category | Rule | Severity | Scan For |
|---|---|---|---|---|
| CQR-200 | command-design | Commands must be task-based | warning | Commands named like UpdateEntity, SetField instead of business tasks like ApproveLoan, SubmitOrder |
| CQR-201 | command-design | Commands must not return query data | error | Command handlers returning domain data beyond ID/version (CQS principle) |
| CQR-202 | command-design | Commands must be immutable | error | Command classes with public setters or mutable fields |
| CQR-203 | command-design | One command per business operation | warning | Multiple commands to complete single business transaction |
| CQR-204 | command-design | Commands must have clear intent | warning | Generic commands like ProcessData or HandleRequest |
| CQR-205 | command-design | Commands should not contain logic | warning | Business logic in command classes instead of handlers |
| CQR-210 | query-design | Queries must not mutate state | error | Query handlers that modify database/aggregate state |
| CQR-211 | query-design | Queries should use read models | warning | Queries reconstructing state from event stream instead of projections |
| CQR-212 | query-design | Query models should be denormalized | warning | Read models with multiple JOINs or complex aggregations |
| CQR-213 | query-design | Queries must be side-effect free | error | Query handlers triggering events, sending emails, or calling external APIs |
| CQR-214 | query-design | Avoid N+1 queries | error | Loops fetching related data one-by-one instead of batch loading |
| CQR-220 | separation | Write models in read operations | error | Query handlers accessing aggregates or write model tables |
| CQR-221 | separation | Read models accepting commands | error | Projections/read models with methods that modify state |
| CQR-222 | separation | Shared models between read/write | warning | Same class/table used for both commands and queries |
| CQR-223 | separation | Direct aggregate queries | error | API endpoints querying aggregates instead of projections |
| CQR-224 | separation | Write-through cache antipattern | error | Updating read model synchronously in command handler |
| CQR-230 | consistency | Missing eventual consistency handling | error | No retry/compensation for failed projection updates |
| CQR-231 | consistency | Assuming immediate consistency | error | Commands followed immediately by queries expecting updated data |
| CQR-232 | consistency | No consistency boundary | warning | Transactions spanning multiple aggregates |
| CQR-233 | consistency | Projection lag not handled | warning | UI not accounting for eventual consistency delays |
| CQR-234 | consistency | Missing idempotency | error | Projectors without duplicate event handling |
| CQR-240 | infrastructure | Same database for read/write | warning | Commands and queries using same tables (may be valid for simple cases) |
| CQR-241 | infrastructure | No command/query routing | error | Direct aggregate access instead of command/query bus |
| CQR-242 | infrastructure | Missing projection rebuild | warning | No mechanism to rebuild read models from events |
| CQR-243 | infrastructure | Synchronous projections in write path | warning | Projection updates blocking command completion |
| CQR-244 | infrastructure | No monitoring of projection lag | warning | No metrics on event-to-projection delay |
| CQR-250 | complexity | CQRS for simple CRUD | error | Using CQRS for entities with only create/read/update/delete operations |
| CQR-251 | complexity | CQRS applied universally | error | Entire application using CQRS instead of specific bounded contexts |
| CQR-252 | complexity | Over-engineered read models | warning | Separate read model for each query instead of shared projections |
| CQR-253 | complexity | Unnecessary event sourcing | warning | Using event sourcing just because CQRS is used |
| CQR-260 | anti-pattern | Value objects in events | error | Event classes containing value object instances instead of primitives |
| CQR-261 | anti-pattern | Chatty commands | warning | Multiple commands for what should be one operation |
| CQR-262 | anti-pattern | Fat events | warning | Events containing entire aggregate state instead of deltas |
| CQR-263 | anti-pattern | Query-command hybrid | error | Single endpoint/method doing both query and command |
| CQR-264 | anti-pattern | Read model as source of truth | error | Business logic depending on projection data |
Event Sourcing
This doctrine audits event sourcing implementation for correctness, performance, and maintainability. Grounded in best practices from Greg Young, Martin Fowler, and Microsoft CQRS/ES guidance.
| ID | Category | Rule | Severity | Scan For |
|---|---|---|---|---|
| EVS-100 | event-design | Events must be immutable | error | Event classes without frozen=True config, or mutable fields, or setter methods |
| EVS-101 | event-design | Events need globally unique IDs | error | Events without unique identifiers (UUID v4/v7, ULID, or sequential per aggregate) |
| EVS-102 | event-design | Event names must be past tense | warning | Event class names not in past tense (e.g., CreateLoan instead of LoanCreated) |
| EVS-103 | event-design | Events must extend base event class | error | Event classes not inheriting from DomainEvent or equivalent base class |
| EVS-104 | event-design | Events must auto-register | warning | Event classes without __init_subclass__ registration or manual registry entry |
| EVS-105 | event-design | Events need occurred_at timestamp | error | Events missing occurred_at field with timezone-aware datetime |
| EVS-106 | event-design | Avoid large event payloads | warning | Event classes with >20 fields or storing full aggregate state instead of deltas |
| EVS-107 | event-design | Event cascade loops | error | Events that directly trigger other events without command mediation (infinite loop risk) |
| EVS-108 | event-design | Over-granular events | warning | Storing every field change as separate event (e.g., NameChanged, EmailChanged vs UserUpdated) |
| EVS-110 | flow | Events must be persisted before publishing | error | Service methods that call bus.publish() before store.append_events() |
| EVS-111 | flow | Uncommitted events must be cleared after save | error | Missing aggregate.clear_uncommitted() after successful append_events() |
| EVS-112 | flow | Events published outside transaction boundary | warning | Event publishing inside database transaction blocks (should be after commit) |
| EVS-113 | flow | Missing optimistic concurrency control | error | EventStore without catching unique violations or version conflicts on append |
| EVS-114 | flow | Synchronous projections blocking writes | warning | Projector handlers called within write transaction (should be async or after) |
| EVS-120 | replay | Event handlers must be idempotent | error | Handlers using +=, .append() without checking for duplicates, or lacking event_id tracking |
| EVS-121 | replay | Replay must be deterministic | error | Event handlers using datetime.now(), random, or external service calls |
| EVS-122 | replay | from_events() must not emit new events | error | from_events() or replay methods that add to uncommitted events list |
| EVS-123 | replay | Event handlers must use _when_* naming | warning | Public methods handling events, or handlers not following _when_<event_name> pattern |
| EVS-124 | replay | Missing handler for registered event | warning | Event type in registry but no corresponding _when_* method in any aggregate |
| EVS-125 | replay | Handler mutating state without event | error | _when_* methods that change state but aggregate method doesn't emit event first |
| EVS-130 | snapshot | Snapshots must include last_event_id | error | to_snapshot() not including last_event_id, or from_snapshot() not restoring it |
| EVS-131 | snapshot | Decimal fields must serialize as strings | error | Snapshot methods using float(decimal_value) instead of str(decimal_value) |
| EVS-132 | snapshot | Snapshot load must have fallback | error | Snapshot loading without try/catch fallback to full event replay |
| EVS-133 | snapshot | Snapshots need periodic cleanup | warning | No cleanup of old snapshots (should keep only N most recent per aggregate) |
| EVS-134 | snapshot | Snapshot interval not configured | warning | No snapshot interval config (Young: every 50-100 events typical) |
| EVS-135 | snapshot | from_snapshot() must validate invariants | warning | from_snapshot() that doesn't validate business rules after reconstruction |
| EVS-140 | projection | Projectors must be idempotent | error | Missing last_event_id check or ON CONFLICT DO NOTHING in projector handlers |
| EVS-141 | projection | Projections storing domain logic | error | Projections with business methods beyond simple getters (logic belongs in aggregates) |
| EVS-142 | projection | Missing projection rebuild script | warning | No script to rebuild projections from event stream |
| EVS-143 | projection | Projectors modifying event data | error | Projector handlers that mutate the event object instead of just reading it |
| EVS-144 | projection | Synchronous projector without error handling | error | Projector in sync path that can crash the write operation on failure |
| EVS-145 | projection | Projector accessing external services | warning | Projectors making HTTP calls or accessing external DBs (couples availability) |
| EVS-150 | store | Event store allows event mutation | error | Event store with UPDATE/DELETE permissions on event table |
| EVS-151 | store | Missing event serialization registry | error | No registry mapping event_type strings to event classes for deserialization |
| EVS-152 | store | Events stored without aggregate_type | error | Event records missing aggregate_type field (needed for filtering) |
| EVS-153 | store | No batch loading support | warning | EventStore without methods to load multiple aggregates in single query |
| EVS-154 | store | No event ordering strategy | error | No way to order events (via timestamp, sequence number, or time-ordered IDs) |
| EVS-155 | store | No event versioning strategy | warning | No version field or schema migration plan for event evolution |
| EVS-160 | evolution | Breaking event schema changes | error | Removing fields from events, renaming without alias, changing field types |
| EVS-161 | evolution | New required fields without defaults | error | Adding non-nullable fields to events without default values |
| EVS-162 | evolution | Missing backward-compatible aliases | warning | Renamed events without keeping old handler name as alias (e.g., _when_payment_received = _when_repayment_received) |
| EVS-163 | evolution | Event upcasting not documented | warning | Schema migrations without documentation of how old events map to new structure |
| EVS-170 | performance | Loading aggregates without snapshots | warning | Services loading aggregates with >100 events but no snapshot support |
| EVS-171 | performance | N+1 queries loading aggregates | error | Service loops calling load_aggregate() instead of using batch loading |
| EVS-172 | performance | Unbounded event queries | warning | Queries fetching all events without pagination or date ranges |
| EVS-173 | performance | Missing indexes on event queries | warning | No indexes on (aggregate_id, event_id), (aggregate_type, occurred_at) pairs |
| EVS-174 | performance | Snapshot on every save | warning | Taking snapshots too frequently (every event) instead of at intervals |
| EVS-175 | performance | Event sourcing applied universally | error | Using ES for entire app (Young: "not a top-level architecture") |
Hexagonal Architecture
Hexagonal Architecture (Ports and Adapters) decouples core business logic from external concerns like databases, UIs, and third-party APIs. By treating the application as a central "inside" surrounded by an "outside," it ensures business rules remain testable in isolation and resilient to infrastructure evolution.
Foundational Works:
Practitioner Guidance:
Anti-Patterns / Failure Cases:
| ID | Category | Rule | Severity | Scan For |
|---|---|---|---|---|
| HEX-001 | dependency-direction | Domain layer must not import Infrastructure | error | import .*infrastructure pattern in domain/ files |
| HEX-002 | dependency-direction | Application layer must not import Infrastructure | error | import .*infrastructure pattern in application/ files |
| HEX-003 | dependency-direction | Domain layer must not import external frameworks | error | `import (fastapi\ |
| HEX-004 | dependency-direction | Ports must not import Infrastructure | error | import .*infrastructure pattern in ports/ files |
| HEX-005 | dependency-direction | Core layers must not import test utilities | error | `import (pytest\ |
| HEX-010 | port-design | Ports must be defined as Interfaces or Abstract Classes | error | Files in ports/ lacking abc.ABC, Protocol, or Interface definitions |
| HEX-011 | port-design | Application services must only interact with Ports | error | Class instantiations of infrastructure classes within application/ |
| HEX-012 | port-design | Ports must use Domain Models or DTOs, never Infra Models | error | Port method signatures using types from sqlalchemy, pydantic.BaseModel, or ORM entities |
| HEX-013 | port-design | Ports must be named after capability, not implementation | warning | Port class names containing technology-specific words (e.g., Sql, Mongo, Http) |
| HEX-014 | port-design | Driven Ports must have at least one implementation | error | Port definition in ports/ with no corresponding implementation in infrastructure/ |
| HEX-020 | adapter-boundary | Adapters must reside in the Infrastructure layer | error | Classes inheriting from Port or Repository found outside infrastructure/ |
| HEX-021 | adapter-boundary | Adapters must not contain core business logic | error | Cyclomatic complexity > 10 in infrastructure/ methods |
| HEX-022 | adapter-boundary | Infrastructure models must be mapped to Domain models | warning | Adapter method returning a third-party or ORM object directly to application/ |
| HEX-023 | adapter-boundary | Adapters must not depend on other Adapters | warning | import of one sub-module in infrastructure/ (e.g., persistence) by another (e.g., web) |
| HEX-024 | adapter-boundary | SQL or NoSQL queries must stay in Adapters | error | SQL strings, Query Builders, or Mongo selectors found outside infrastructure/ |
| HEX-030 | core-purity | Domain Entities must not perform IO | error | Calls to print, logging (to file/network), or socket operations in domain/ |
| HEX-031 | core-purity | Application services must be orchestration-focused | warning | Files in application/ with > 400 lines of code |
| HEX-032 | core-purity | Domain logic must not leak into Controllers | error | If/Else logic or calculations found in infrastructure/web/controllers |
| HEX-033 | core-purity | Business logic must not be in Ports | error | Concrete method logic (non-abstract) inside ports/ files |
| HEX-034 | core-purity | Use Cases must be atomic units of work | warning | application/ methods calling more than 3 different Driven Ports |
| HEX-040 | mapping-integrity | Domain must not be aware of persistence IDs | warning | Use of database-generated id fields (auto-increment) as primary keys in domain/ entities |
| HEX-041 | mapping-integrity | DTOs must be used for cross-boundary communication | error | Domain Entities used as request/response bodies in infrastructure/api |
| HEX-042 | mapping-integrity | Explicit mappers are required | warning | Direct attribute copying (a.x = b.x) in application services instead of using a Mapper class/function |
Messaging & Async Communication
This doctrine audits message-driven architecture, ensuring reliable async communication, proper error handling, and correct implementation of messaging patterns.
Foundational Works:
Anti-Patterns & Pitfalls:
Broker-Specific Guidance:
Delivery Guarantees:
| ID | Category | Rule | Severity | Scan For |
|---|---|---|---|---|
| MSG-001 | message-design | Messages must be immutable | error | Message classes with setters, mutable fields, or dataclass without frozen=True |
| MSG-002 | message-design | Messages need unique identifiers | error | Messages without ID field (UUID, ULID, or correlation_id) |
| MSG-003 | message-design | Message size must be bounded | warning | Message classes with unbounded collections or >100KB typical payload |
| MSG-004 | message-design | Avoid sensitive data in messages | error | Messages containing passwords, tokens, SSN, credit cards (scan for field names/patterns) |
| MSG-005 | message-design | Messages must be versioned | warning | Message classes without version field or schema registry |
| MSG-006 | message-design | Use correlation IDs for tracing | warning | Messages without correlation_id for distributed tracing |
| MSG-007 | message-design | Avoid entity-based events | warning | Events named like UserUpdated instead of business events like UserRegistered, EmailChanged |
| MSG-010 | delivery | Handlers must be idempotent | error | Message handlers without idempotency checks (no dedup by message_id) |
| MSG-011 | delivery | Missing acknowledgement handling | error | Consumers not calling ack/nack explicitly (auto_ack=True in production) |
| MSG-012 | delivery | No retry configuration | warning | Publishers/consumers without retry logic or exponential backoff |
| MSG-013 | delivery | Missing dead letter queue | warning | Queue declarations without DLQ configuration for failed messages |
| MSG-014 | delivery | Unbounded retries | error | Retry loops without max attempts or without backing off |
| MSG-015 | delivery | No timeout configuration | warning | RPC-style messaging without timeout (can block forever) |
| MSG-016 | delivery | Guaranteed delivery not configured | warning | Critical messages without persistence (delivery_mode=2 in RabbitMQ) |
| MSG-020 | error-handling | No poison message handling | error | Consumers without try/catch around processing or DLQ for failures |
| MSG-021 | error-handling | Swallowing exceptions | error | Catch blocks that don't log/rethrow/nack messages |
| MSG-022 | error-handling | Missing circuit breaker | warning | Consumers without circuit breaker for downstream service calls |
| MSG-023 | error-handling | No compensation logic | warning | Saga participants without compensating transactions |
| MSG-024 | error-handling | Synchronous error propagation | error | Publishing error messages synchronously in catch blocks (blocks recovery) |
| MSG-025 | error-handling | Missing monitoring/alerting | warning | No metrics for queue depth, consumer lag, or failure rates |
| MSG-030 | ordering | Assuming message order | error | Logic that depends on message order without explicit sequencing |
| MSG-031 | ordering | Race conditions in saga | error | Saga steps that can execute out of order without guards |
| MSG-032 | ordering | Missing version checks | warning | Updates without optimistic concurrency control |
| MSG-033 | ordering | Competing consumers issues | warning | Shared state modified by multiple consumers without locking |
| MSG-034 | ordering | No sequence numbers | warning | Related messages without sequence/version for ordering |
| MSG-040 | infrastructure | Hardcoded broker URLs | error | Connection strings in code instead of environment variables |
| MSG-041 | infrastructure | No connection pooling | warning | Creating new connections per message instead of reusing |
| MSG-042 | infrastructure | Missing heartbeat config | warning | Long-running consumers without heartbeat configuration |
| MSG-043 | infrastructure | No graceful shutdown | error | Consumers without signal handlers for clean shutdown |
| MSG-044 | infrastructure | Synchronous publishing in transaction | warning | Publishing messages inside database transactions (2PC issues) |
| MSG-045 | infrastructure | No outbox pattern | warning | Direct publishing without outbox table (dual write problem) |
| MSG-046 | infrastructure | Queue/topic name collisions | error | Hardcoded queue names without environment/tenant prefixes |
| MSG-050 | patterns | Sync over async antipattern | error | Blocking waiting for async response (request-reply with timeout) |
| MSG-051 | patterns | Chatty messaging | warning | Multiple messages for single logical operation |
| MSG-052 | patterns | Fat messages | warning | Messages >1MB or containing full entities instead of IDs |
| MSG-053 | patterns | Missing saga orchestrator | error | Distributed transactions without coordinator |
| MSG-054 | patterns | Two-phase commit over messaging | error | XA transactions spanning message broker and database |
| MSG-055 | patterns | Temporal coupling | warning | Expecting immediate response from async operations |
| MSG-060 | performance | No batching | warning | Publishing/consuming one message at a time in loops |
| MSG-061 | performance | Prefetch not configured | warning | Consumers without prefetch limit (can overwhelm worker) |
| MSG-062 | performance | No async I/O | warning | Blocking I/O in message handlers (use async/await or threads) |
| MSG-063 | performance | Unbounded queues | warning | No max length or TTL on queues (memory issues) |
| MSG-064 | performance | No consumer scaling | warning | Fixed number of consumers regardless of queue depth |
Saga Pattern
This doctrine audits saga implementations for distributed transactions, ensuring proper compensation logic, isolation handling, and coordination patterns in microservices architectures.
Foundational Works:
Implementation Approaches:
Anti-Patterns & Limitations:
Practical Guides:
| ID | Category | Rule | Severity | Scan For |
|---|---|---|---|---|
| SAG-001 | design | Saga overuse indicates design smell | warning | >30% of use cases require sagas (services organized by entities) |
| SAG-002 | design | Missing saga definition | error | Distributed transactions without explicit saga coordinator or choreography |
| SAG-003 | design | Mixing orchestration and choreography | error | Same saga using both patterns (events AND commands from orchestrator) |
| SAG-004 | design | Saga for technical errors | error | Compensating transactions triggered by network/timeout errors |
| SAG-005 | design | No saga state machine | warning | Saga without explicit state transitions (PENDING→PROCESSING→COMPLETED/FAILED) |
| SAG-006 | design | Unbounded saga duration | warning | Sagas without timeout or maximum duration |
| SAG-007 | design | Nested sagas | error | Saga triggering another saga (composition complexity) |
| SAG-010 | compensation | Missing compensating transaction | error | Saga steps without corresponding compensation logic |
| SAG-011 | compensation | Non-idempotent compensation | error | Compensating transactions without idempotency checks |
| SAG-012 | compensation | Compensation throws exceptions | error | Compensating transactions that can fail without retry |
| SAG-013 | compensation | Compensation order incorrect | error | Compensations not executed in reverse order |
| SAG-014 | compensation | No compensation timeout | warning | Compensating transactions without timeout handling |
| SAG-015 | compensation | Incomplete compensation | error | Compensation doesn't fully undo the forward action |
| SAG-016 | compensation | Compensation side effects | warning | Compensating transactions triggering new business logic |
| SAG-020 | isolation | No isolation strategy | error | Concurrent sagas modifying same entities without countermeasures |
| SAG-021 | isolation | Missing semantic locks | warning | No application-level locks for business resources |
| SAG-022 | isolation | No dirty read prevention | warning | Reading uncommitted saga changes without versioning |
| SAG-023 | isolation | No saga correlation | error | Cannot track which changes belong to which saga |
| SAG-024 | isolation | Race condition in compensation | error | Compensation can race with normal operations |
| SAG-025 | isolation | No version checks | warning | Updates without checking if data changed during saga |
| SAG-030 | orchestration | Single point of failure | warning | Orchestrator without high availability setup |
| SAG-031 | orchestration | Orchestrator contains business logic | error | Business rules in orchestrator instead of services |
| SAG-032 | orchestration | No orchestrator persistence | error | Orchestrator state only in memory |
| SAG-033 | orchestration | Synchronous orchestration | error | Orchestrator making blocking calls to services |
| SAG-034 | orchestration | No orchestrator monitoring | warning | No metrics/alerting for orchestrator health |
| SAG-035 | orchestration | Orchestrator versioning missing | warning | No strategy for upgrading running sagas |
| SAG-040 | choreography | Cyclic dependencies | error | Services consuming each other's events in cycles |
| SAG-041 | choreography | No event correlation | error | Events missing saga_id/correlation_id |
| SAG-042 | choreography | Implicit flow | warning | Saga flow not documented/discoverable |
| SAG-043 | choreography | Complex choreography | warning | >4 services in choreographed saga |
| SAG-044 | choreography | Missing event handlers | error | Service not handling expected saga events |
| SAG-045 | choreography | Event ordering assumptions | error | Choreography assuming event order without guarantees |
| SAG-050 | state | No saga persistence | error | Saga state not persisted to durable storage |
| SAG-051 | state | State machine violations | error | Invalid state transitions (COMPLETED→PROCESSING) |
| SAG-052 | state | Lost saga instances | error | No mechanism to recover in-flight sagas after crash |
| SAG-053 | state | No saga history | warning | Cannot reconstruct what happened in saga |
| SAG-054 | state | Saga state in multiple places | error | State split across orchestrator and services |
| SAG-055 | state | No idempotency tokens | error | Saga steps without idempotency tokens |
| SAG-060 | testing | No integration tests | error | Saga without end-to-end tests |
| SAG-061 | testing | No failure scenario tests | error | Tests don't cover compensation paths |
| SAG-062 | testing | No concurrent saga tests | warning | Tests don't verify isolation/conflicts |
| SAG-063 | operations | No saga observability | error | Cannot trace saga execution across services |
| SAG-064 | operations | No manual intervention | warning | No way to manually complete/compensate stuck sagas |
| SAG-065 | operations | No saga metrics | warning | No monitoring of saga success/failure rates |
Backend For Frontend (BFF) Architecture
The BFF doctrine enforces the creation of specialized backend layers for specific user interfaces. It ensures that core domain services remain clean and "client-agnostic" while providing a tailored, high-performance experience for diverse clients (e.g., Mobile, Web, IoT).
Foundational Works:
Practitioner Guidance:
Anti-Patterns / Failure Cases:
| ID | Category | Rule | Severity | Scan For |
|---|---|---|---|---|
| BFF-001 | logic-leak | Domain business logic must not reside in the BFF | error | Calculation or validation logic in bff/ that isn't purely about formatting |
| BFF-002 | persistence-leak | A BFF must not have its own database or direct DB access | error | Import of ORMs, SQL clients, or database drivers inside bff/ |
| BFF-003 | cross-bff-dependency | BFFs must not depend on or call other BFFs | error | import <pkg>.bff.mobile found in bff/web/ |
| BFF-004 | direct-core-bypass | Clients must not bypass the BFF to call Core Services directly | warning | Frontend code containing direct URLs to services/core/ |
| BFF-005 | shared-bff | A single BFF must not serve multiple diverse client types | warning | One BFF handling both Mobile and Web if payloads are >30% different |
| BFF-021 | model-coupling | BFFs must not use Core Service DTOs as their own API response | error | bff/*/api/ methods returning classes defined in services/core/ |
| BFF-022 | missing-mapper | Every BFF endpoint should use a dedicated mapper | warning | Controllers in bff/ performing inline data transformation |
| BFF-023 | polymorphic-leak | BFF should hide internal type hierarchies from the UI | warning | Returning internal class type names or discriminator fields in JSON |
| BFF-024 | invalid-date-format | BFF must standardize dates to a client-preferred format | warning | Returning raw DB timestamps instead of ISO-8601 or localized strings |
| BFF-025 | enum-leak | Do not expose internal service Enums directly to the UI | warning | Mapping core Enums 1:1 without a stable BFF-specific Enum |
| BFF-006 | synchronous-bottleneck | Downstream calls should be executed in parallel where possible | warning | Sequential await statements for independent data sources |
| BFF-007 | missing-aggregation | BFFs should provide aggregated endpoints for complex UI views | warning | UI making multiple BFF calls to load one screen instead of one composite call |
| BFF-008 | payload-bloat | BFF responses must only contain fields required by the UI | error | BFF returning 1:1 copies of Core Service entities without filtering |
| BFF-009 | excessive-hop-count | BFF endpoints should not call more than 10 downstream services | warning | A single BFF request triggering > 10 internal network calls |
| BFF-010 | missing-pagination | BFF must forward or implement pagination for large lists | error | Endpoints returning arrays without limit, offset, or cursor |
| BFF-035 | thread-leak | Async operations must use managed thread pools | error | Usage of new Thread() or unmanaged CompletableFuture in BFF logic |
| BFF-036 | connection-monopoly | Limit the number of concurrent calls to a single Core service | warning | Lack of bulkhead configuration for high-traffic downstream services |
| BFF-037 | unconstrained-buffers | Limit the size of incoming request bodies in the BFF | error | Missing max-payload-size configuration in BFF settings |
| BFF-038 | event-loop-block | BFF must not perform synchronous IO on the main event loop | error | fs.readFileSync or similar blocking calls in Node.js/Netty BFFs |
| BFF-011 | secret-exposure | Downstream API keys/secrets must not be exposed to the UI | error | BFF responses containing raw internal tokens or service-to-service keys |
| BFF-012 | token-passing | BFF must translate client auth to internal service auth | error | Passing UI cookies/session-ids directly to downstream domain services |
| BFF-013 | missing-cors-policy | Every BFF must have a strict, client-specific CORS policy | error | Usage of Access-Control-Allow-Origin: * in any bff/ configuration |
| BFF-014 | plaintext-transmission | PII or sensitive data must be encrypted if the client is public | warning | Lack of field-level encryption for sensitive fields in the mappers/ layer |
| BFF-015 | inadequate-sanitization | BFF must sanitize all inputs before forwarding to Core | error | Direct forwarding of raw UI request bodies to downstream PUT/POST calls |
| BFF-046 | cross-region-leak | BFF should live in the same region as the client's data | warning | A US-based BFF calling EU-based core services for EU users |
| BFF-047 | excessive-pii-handling | BFF should only "see" PII it absolutely needs for display | warning | Mapping logic that touches PII fields not used in the final response |
| BFF-048 | missing-consent-check | BFF must respect user data consent flags | error | Returning "Marketing" data through the BFF when the consent_flag is false |
| BFF-049 | unmasked-id | Mask or obfuscate internal database IDs in public responses | warning | Returning raw integer Auto-increment IDs instead of HashIDs or UUIDs |
| BFF-050 | static-client-coupling | Avoid hard-coupling BFF DTOs to specific UI components | warning | Naming BFF DTO fields after UI elements (e.g., SubmitButtonLabel) |
| BFF-016 | missing-circuit-breaker | Downstream calls must use circuit breakers | error | Client calls in bff/*/clients/ missing a resilience wrapper |
| BFF-017 | missing-timeout | Every downstream call must have a strict timeout (< 2s) | error | Client instantiations without an explicit readTimeout |
| BFF-018 | missing-fallback | BFF endpoints must define a fallback for failed downstream calls | warning | Lack of "graceful degradation" (e.g., returning cached or empty data) |
| BFF-019 | retry-storm | BFF retries must use exponential backoff and jitter | error | Simple loop retries found in bff/ client logic |
| BFF-020 | bulkhead-isolation | Use bulkheads to prevent one downstream service from killing the BFF | warning | Lack of dedicated thread pools or semaphores per downstream client |
| BFF-026 | stateful-bff | BFFs should be stateless to allow horizontal scaling | error | Usage of local file system or in-memory sessions for user data |
| BFF-027 | missing-header-forwarding | Cache-Control headers from Core should be respected/forwarded | warning | BFF ignoring downstream ETag or Cache-Control headers |
| BFF-028 | over-caching | Sensitive user data must not be cached at the BFF level | error | Caching responses containing PII without a user-specific cache key |
| BFF-029 | distributed-cache-leak | Avoid sharing a single cache instance between different BFFs | warning | bff/web and bff/mobile using the same Redis namespace/prefix |
| BFF-030 | trace-interruption | BFFs must propagate Correlation IDs | error | Downstream client calls that do not forward the X-Correlation-ID |
| BFF-031 | opaque-error-mapping | Translate downstream errors into UI-friendly codes | warning | Passing raw 500 or 403 errors from Core directly to the UI |
| BFF-032 | missing-golden-signals | BFFs must expose Rate, Error, and Duration metrics | error | Absence of Prometheus or OpenTelemetry metrics for BFF endpoints |
| BFF-033 | logs-in-production | Avoid logging PII or raw payloads in production logs | error | Log statements in bff/ printing request.body or response.data |
| BFF-034 | slow-mapper | Mapping logic must not exceed 50ms per request | warning | Complex recursive mappers in bff/*/mappers/ causing latency spikes |
| BFF-039 | version-mismatch | BFF should be deployable independently of Core Services | warning | Build scripts requiring a synchronized deploy of bff/ and services/core/ |
| BFF-040 | orphaned-bff | BFF must be owned by the UI team, not the Core team | warning | Code review requirements for bff/ not including Frontend developers |
| BFF-041 | hardcoded-core-urls | Core service URLs must be injected via environment/discovery | error | Hardcoded http://core-service:8080 strings in BFF client code |
| BFF-042 | missing-contract-tests | BFF must have contract tests against the Core API | error | Absence of Pact or Consumer Driven Contract tests in bff/*/tests/ |
| BFF-043 | legacy-bloat | Remove BFF endpoints that are no longer used by the UI | warning | Endpoints in bff/ with zero recorded traffic in the last 30 days |
| BFF-044 | monolithic-bff-structure | Every client must have a physically isolated BFF directory | error | bff/ containing all client logic without web/, mobile/ sub-folders |
| BFF-045 | bypass-validation | BFF must validate UI inputs before hitting the Core network | error | Lack of validation annotations on BFF API DTOs |
Layered (N-Tier) Architecture
The Layered (N-Tier) Architecture doctrine enforces strict horizontal separation of concerns. It ensures that changes in low-level details (like databases) do not leak into high-level business logic, and that user interface concerns remain isolated from data persistence.
Foundational Works:
Practitioner Guidance:
Anti-Patterns / Failure Cases:
| ID | Category | Rule | Severity | Scan For |
|---|---|---|---|---|
| LNT-001 | dependency-direction | Lower layers must not depend on higher layers | error | import <pkg>.presentation in business/ or persistence/ |
| LNT-002 | dependency-skip | Layers should not skip immediate neighbors | warning | import <pkg>.persistence directly in presentation/ bypassing business/ |
| LNT-003 | circular-dependency | Bi-directional dependencies between layers are forbidden | error | Circular import paths between business/ and persistence/ |
| LNT-004 | external-leak | Third-party UI or DB libraries must not leak into business logic | error | import of web frameworks (e.g., Express, Spring Web) or ORMs in business/ |
| LNT-005 | direct-instantiation | Layers must not instantiate their own dependencies | warning | Usage of new Service() or new Repository() instead of Dependency Injection |
| LNT-006 | leaky-abstraction | Persistence-specific exceptions must not reach the presentation layer | error | catch blocks in presentation/ handling SQLException or ORM-specific errors |
| LNT-007 | model-bleeding | Database entities must not be used as API responses | warning | Method signatures in presentation/ controllers returning classes defined in persistence/ |
| LNT-008 | logic-placement | Business logic must not reside in the presentation layer | error | presentation/ files containing complex conditionals, math, or data transformation > 15 lines |
| LNT-009 | logic-displacement | Business logic must not reside in the persistence layer | error | persistence/ classes containing non-query logic (e.g., tax calculation) |
| LNT-010 | direct-db-access | UI must not execute raw SQL or DB commands | error | SQL strings or DB client calls (e.g., db.query()) inside presentation/ |
| LNT-011 | excessive-exposure | Internal service methods must be private or protected | warning | Public methods in business/ that are not called by presentation/ |
| LNT-012 | missing-abstraction | Business layer should access persistence via interfaces | warning | business/ classes instantiating concrete persistence/ classes instead of using DI |
| LNT-013 | contract-bypass | Business methods must be used instead of direct data manipulation | error | presentation/ modifying objects and calling persistence.save() directly |
| LNT-014 | service-bloat | Business services should not exceed complexity thresholds | warning | Service classes in business/ with > 10 public methods or > 500 lines of code |
| LNT-015 | utility-misplacement | Utilities in common/ must be logic-free | warning | common/ files containing domain-specific rules or database access |
| LNT-016 | global-state | Layers must not communicate via global shared variables | error | Use of global or static variables to pass data between layers |
| LNT-017 | connection-leak | Persistence layer must manage its own connection lifecycle | error | Connection objects (e.g., SqlConnection) passed as arguments into business/ |
| LNT-018 | transaction-leak | Transaction boundaries should be managed in the Business layer | warning | commit() or rollback() calls appearing inside presentation/ or persistence/ |
| LNT-019 | session-bleeding | HTTP session objects must not be passed to the business layer | error | HttpServletRequest or Session objects found in business/ method signatures |
| LNT-020 | sinkhole-pattern | Avoid services that only delegate to the layer below | warning | Methods in business/ that consist of a single return call to persistence/ |
| LNT-021 | fat-controller | Controllers must delegate to business services | error | presentation/ controllers exceeding 3 injected dependencies or 200 lines |
| LNT-022 | smart-ui | UI components must not contain data validation logic | warning | UI-tier code performing complex domain validation instead of calling business/ |
| LNT-023 | anemic-domain | Ensure Business layer contains logic, not just getters/setters | warning | business/ folder consisting entirely of DTOs with no logic-bearing services |
| LNT-024 | lasagne-architecture | Do not create unnecessary sub-layers | warning | A single request path crossing > 5 layer boundaries |
| LNT-025 | silent-failure | Layers must not swallow exceptions without logging or rethrowing | error | Empty catch blocks or catch blocks that only log without re-escalation |
| LNT-026 | generic-exceptions | Layers must throw specific custom exceptions | warning | Usage of throw new Exception() or throw new RuntimeException() |
| LNT-027 | validation-bypass | Persistence must not be called without going through validation | error | Call sites for persistence/ methods found outside of business/ |
| LNT-028 | unit-test-isolation | Business layer unit tests must use mocks for persistence | error | Unit tests in business/ that attempt to connect to a real database |
| LNT-029 | missing-layer-tests | Each layer should have a corresponding test suite | warning | A layer directory (e.g., persistence/) with 0 matching files in tests/ |
| LNT-030 | integration-test-scope | Integration tests must span at least two layers | warning | Tests labeled "integration" that only exercise a single isolated class |
| LNT-031 | hardcoded-config | Configuration values must not be hardcoded in layers | error | Hardcoded DB URLs, API keys, or environment-specific flags in business/ |
| LNT-032 | circular-init | Service initialization must not contain circular dependencies | error | constructor calls that eventually lead back to the same class during startup |
| LNT-033 | side-effect-ctor | Constructors must not perform heavy IO or logic | warning | constructor methods containing database queries or network calls |
| LNT-034 | static-dependency | Avoid static method calls for cross-layer logic | warning | business/ calling static methods in persistence/ (hinders mockability) |
| LNT-035 | improper-common-dep | Common layer must not depend on any other layer | error | import <pkg>.business or import <pkg>.persistence in common/ |
| LNT-036 | race-condition | Business services should be stateless to ensure thread safety | error | Non-final instance variables in business/ services that are modified after init |
| LNT-037 | missing-optimistic-lock | Updates must handle concurrent modifications | warning | persistence/ update methods that do not use a version column or timestamp |
| LNT-038 | async-leak | Background tasks must not outlive the request scope in presentation | warning | async task creation or Thread / ExecutorService usage in presentation/ that stores references in objects with a lifecycle longer than the current request |
Microservices Architecture
The Microservices Architecture doctrine enforces the decentralization of data, logic, and deployments. It ensures that services remain loosely coupled and independently scalable, preventing the formation of a "distributed monolith" where changes in one service require synchronized deployments across the entire fleet.
Foundational Works:
Practitioner Guidance:
Anti-Patterns / Failure Cases:
| ID | Category | Rule | Severity | Scan For |
|---|---|---|---|---|
| MCR-001 | data-sovereignty | Services must not share a database schema | error | Multiple services in services/ connecting to the same DB schema/catalog |
| MCR-002 | direct-domain-access | Services must not import domain logic from other services | error | import <pkg>.services.serviceA.domain in services/serviceB/ |
| MCR-003 | shared-state | Services must not share mutable global state | error | Hardcoded shared keys or global caches accessed by multiple service roots |
| MCR-004 | circular-dependency | Circular synchronous dependencies between services are forbidden | error | Service A calls B via API, and Service B calls A via API |
| MCR-005 | leaky-persistence | Internal DB primary keys must not be exposed in public APIs | warning | Entity ID fields (UUID/Serial) from data/ used directly in api/ responses |
| MCR-026 | distributed-transaction | Do not use cross-service atomic locks or 2PC | error | Usage of JTA, XA transactions, or cross-service mutexes |
| MCR-027 | missing-compensation | Async operations must have compensation actions | error | Event listeners without a failure-handling/rollback path |
| MCR-028 | ghost-writes | Ensure idempotency for all event consumers | error | Event handlers that perform writes without checking duplicate message IDs |
| MCR-029 | dual-write | Do not write to DB and Broker in one local transaction | warning | Logic calling db.save() and broker.publish() sequentially |
| MCR-030 | outbox-bypass | Use Outbox pattern for guaranteed message delivery | warning | Direct publishing to broker from business logic instead of Outbox table |
| MCR-006 | synchronous-chaining | Avoid deep chains of synchronous HTTP/gRPC calls | warning | Service call depth > 3 in a single request trace (e.g., A -> B -> C -> D) |
| MCR-007 | missing-contract | API changes must be governed by explicit versioning | error | Breaking changes to api/ definitions without a version increment |
| MCR-008 | hardcoded-endpoints | Service locations must not be hardcoded | error | IP addresses or static DNS names for other services in configuration files |
| MCR-009 | client-library-leak | Service client libraries must not leak internal models | warning | Client SDKs in shared-libraries/ exposing internal DB entities |
| MCR-010 | untyped-payloads | Inter-service communication must use structured schemas | error | Use of JSON.parse or generic Maps for API payloads without schema validation |
| MCR-011 | logic-in-shared | Business logic must not reside in shared libraries | error | shared-libraries/ containing domain-specific validation or logic |
| MCR-012 | shared-lib-bloat | Shared libraries must be versioned and kept lean | warning | shared-libraries/ exceeding 1000 LOC or including heavy dependencies |
| MCR-013 | version-lock | Services must not be forced into a single global library version | warning | Root-level build files enforcing one version for all services/ |
| MCR-014 | transitive-leak | Shared libraries must not expose transitive deps to services | error | Services relying on a library provided implicitly by a shared parent |
| MCR-015 | binary-coupling | Avoid sharing compiled DTOs across services | warning | Services sharing a .jar or .dll containing data transfer objects |
| MCR-016 | missing-timeout | Every inter-service call must have an explicit timeout | error | HTTP/gRPC client instantiations without a defined timeout duration |
| MCR-017 | missing-circuit-breaker | External service calls must be wrapped in circuit breakers | error | Inter-service calls lacking a circuit-breaker implementation |
| MCR-018 | chatty-api | Avoid fine-grained calls where one aggregate call suffices | warning | Loop constructs making repeated API calls to another service |
| MCR-019 | blocking-io | Prefer async communication for non-query operations | warning | Synchronous POST/PUT calls where an event-driven approach is viable |
| MCR-020 | retry-storm | Retries must implement exponential backoff and jitter | error | Retry logic with constant intervals (e.g., retry(3, 1000ms)) |
| MCR-021 | missing-trace-id | Correlation IDs must be propagated through all calls | error | API handlers in services/*/api/ not forwarding X-Correlation-ID |
| MCR-022 | inconsistent-logging | Services must use a standardized structured logging format | warning | Log statements not using the approved JSON schema |
| MCR-023 | missing-health-check | Every service must expose a /health or /ready endpoint | error | Absence of standard health check route in the service's API |
| MCR-024 | opaque-failures | Services must return standard error codes (RFC 7807) | warning | Non-standard or generic 500 errors without diagnostic context |
| MCR-025 | missing-metrics | Services must expose standard golden signals (Rate/Errors/Dur) | error | Lack of Prometheus/Metrics endpoints in service initialization |
| MCR-031 | secrets-in-code | Secrets must never be stored in source code | error | Presence of API_KEY or SECRET strings in services/ files |
| MCR-032 | environment-pollution | Services must not rely on host-specific env vars | error | Hardcoded references to local machine paths or developer env vars |
| MCR-033 | config-drift | Configurations must be versioned alongside code | error | Services relying on unversioned external config stores (e.g., raw Etcd keys) |
| MCR-034 | missing-default-config | Every service must provide a safe local config | warning | Service failure to start without a connection to a remote config server |
| MCR-035 | manual-deployment | Services must have an automated CI/CD pipeline | error | Absence of Jenkinsfile, .github/workflows, or gitlab-ci.yml |
| MCR-036 | static-scaling | Services should not have hardcoded instance counts | warning | Deployment manifests with fixed replicas: X instead of HPA |
| MCR-037 | stateful-service | Services should be stateless for horizontal scaling | error | Usage of local file system or in-memory sessions for persistence |
| MCR-038 | unconstrained-resources | Manifests must define CPU/Memory limits | error | Manifests without resources.limits or resources.requests |
| MCR-039 | hardcoded-image-tags | Deployment manifests must not use latest tags | error | Usage of image: my-service:latest in deployment files |
| MCR-040 | sidecar-bypass | Services must use the designated service mesh for egress | warning | Direct socket calls bypassing the local mesh proxy (if configured) |
| MCR-041 | missing-contract-tests | Inter-service APIs must be covered by contract tests | error | Lack of Pact or Spring Cloud Contract files in services/*/tests/ |
| MCR-042 | slow-tests | Service unit tests must not exceed 5-minute execution | warning | Local test suites taking > 300s to complete |
| MCR-043 | fragile-integration | Integration tests should not depend on "live" dependencies | error | Tests in services/ requiring a connection to a real Production/Staging DB |
| MCR-044 | missing-load-test | High-traffic services must have defined load test scripts | warning | Absence of k6, Gatling, or JMeter scripts for ingest services |
| MCR-045 | shadow-deployment-lacking | Major API changes must support canary or shadow traffic | warning | Lack of feature flags or traffic routing rules for new major versions |
Modular Monolith Architecture
The Modular Monolith doctrine enforces strict logical isolation within a single deployment unit. It aims to provide the organizational benefits of microservices (team autonomy, clear boundaries) without the "distributed systems tax" of network latency and extreme operational complexity.
Foundational Works:
Practitioner Guidance:
Anti-Patterns / Failure Cases:
| ID | Category | Rule | Severity | Scan For |
|---|---|---|---|---|
| MOM-001 | internal-leak | Modules must not import from another module's internal folder | error | import <pkg>.modules.moduleA.internal in modules/moduleB/ |
| MOM-002 | api-bypass | All inter-module calls must go through the designated API package | error | import of any non-API class or interface from a sibling module |
| MOM-003 | circular-module-dep | Circular dependencies between functional modules are forbidden | error | Module A depends on Module B, and Module B depends on Module A |
| MOM-004 | shared-domain-leak | Modules must not share internal domain entities or value objects | warning | modules/moduleA/internal using a domain class defined in modules/moduleB/internal |
| MOM-005 | deep-nesting | Functional modules should not be nested within other modules | warning | Directory structure exceeding modules/<name>/<layer> (e.g., modules/a/b/c) |
| MOM-006 | cross-module-join | Modules must not perform SQL joins across module boundaries | error | SQL queries in modules/moduleA joining tables owned by modules/moduleB |
| MOM-007 | shared-table | No database table should be modified by more than one module | error | Write/Update operations on table_x occurring in multiple module directories |
| MOM-008 | foreign-key-leak | Hard foreign keys across module boundaries should be avoided | warning | Database schema in moduleA defining a FOREIGN KEY to a table in moduleB |
| MOM-009 | direct-repo-access | A module must not use another module's Persistence/DAO layer | error | modules/moduleA instantiating or injecting a Repository from modules/moduleB |
| MOM-010 | transaction-bypass | Use local module transactions instead of global monolith transactions | warning | Cross-module calls that assume an open transaction from the caller |
| MOM-011 | excessive-sync-calls | Prefer internal events over synchronous calls for side effects | warning | Synchronous API call in moduleA that triggers a heavy write in moduleB |
| MOM-012 | missing-event-contract | Inter-module events must use shared DTOs, not internal types | error | Modules publishing events containing classes from their internal/ package |
| MOM-013 | event-loop | Modules should not create infinite event loops | error | Module A publishes E1 -> Module B receives and publishes E2 -> Module A receives |
| MOM-014 | synchronous-coupling | Module startup must not be blocked by other modules | warning | A module's init() or start() method waiting for a response from another module |
| MOM-015 | blocking-event-bus | Event subscribers should not block the main event bus | error | Event handlers in internal/ performing heavy IO without async wrappers |
| MOM-016 | platform-logic-leak | Business logic must not reside in the platform directory | error | platform/ containing domain-specific rules (e.g., calculateVAT) |
| MOM-017 | platform-dependency | Platform code must not depend on functional modules | error | import <pkg>.modules in any platform/ file |
| MOM-018 | utility-overuse | Avoid "Common" modules that become a dumping ground | warning | platform/common or modules/shared exceeding 2000 lines of code |
| MOM-019 | transitive-dependency | Modules must not rely on transitive dependencies from the platform | warning | A module using a library provided implicitly by a platform dependency |
| MOM-020 | platform-bloat | Platform layer must not exceed 20% of the total codebase size | warning | Codebase-wide LOC check: platform/ vs total project LOC |
| MOM-021 | cross-module-di | Modules must not inject internal classes from other modules | error | Dependency Injection of a class from modules/moduleB/internal into moduleA |
| MOM-022 | missing-interface-binding | API services should be requested by interface, not implementation | warning | moduleA injecting ModuleBServiceImpl instead of IModuleBService |
| MOM-023 | hidden-initialization | Modules must have a clear entry point for lifecycle management | warning | Modules using "magic" static initializers instead of an explicit onStart() hook |
| MOM-024 | circular-init | Module initialization must be acyclic | error | Constructor chains that lead back to the same module during startup |
| MOM-025 | missing-graceful-shutdown | Modules must implement a cleanup/shutdown hook | warning | Modules holding resources (sockets/files) without an onStop() implementation |
| MOM-026 | thread-monopoly | A module must not spawn unmanaged threads | error | Usage of new Thread() or Executors outside of the platform/ managed pool |
| MOM-027 | memory-hog | Large objects (>100MB) must be cleared or managed in a specific module scope | warning | In-memory caches in internal/ folders that do not have an eviction policy |
| MOM-028 | connection-leak | Modules must use namespaced connection pools | warning | A single module opening > 50 concurrent DB connections without explicit config |
| MOM-029 | unconstrained-io | Filesystem access must be scoped to a module-specific directory | error | modules/moduleA writing to a path used by modules/moduleB |
| MOM-030 | compute-monopoly | Background tasks must be priority-labeled per module | warning | Usage of high-priority scheduler flags for non-critical module tasks |
| MOM-031 | static-state-leak | Modules must not store request data in static variables | error | Usage of static fields to hold domain data, causing leaks between module calls |
| MOM-032 | mutable-dto | DTOs passed between modules must be immutable | warning | Classes in modules/*/api/ containing setter methods or mutable collections |
| MOM-033 | unprotected-concurrency | Shared module services must be thread-safe | error | Non-final instance variables in internal/ services modified after initialization |
| MOM-034 | module-test-leak | Tests for Module A must not depend on the internals of Module B | error | modules/moduleA/tests importing from modules/moduleB/internal |
| MOM-035 | missing-module-isolation | Unit tests should be executable per module | warning | Inability to run tests for a single folder in modules/ in isolation |
| MOM-036 | deep-stubbing | Do not stub the internal logic of other modules | warning | Tests in Module A using deep mocks for private methods in Module B |
| MOM-037 | missing-api-tests | Every public API class must have a corresponding contract test | warning | Classes in modules/*/api/ with 0 test coverage in the tests/ folder |
| MOM-038 | integration-flakiness | Integration tests must not depend on global shared state | error | Tests that fail if run in parallel due to database state clashes between modules |
| MOM-039 | anonymous-logs | Every log statement must include the originating module name | error | Log calls that do not include a module context or tag (e.g., [Billing] ...) |
| MOM-040 | hidden-exceptions | Cross-module exceptions must be wrapped with module context | warning | Module A throwing a generic error that hides the fact it originated in Module B |
| MOM-041 | missing-module-metrics | Each module must expose independent success/failure metrics | error | Global "Total Errors" metric without breakdown by module folder |
| MOM-042 | trace-bypass | Cross-module calls must maintain a single trace ID | warning | Calls to Module API that do not propagate the current Correlation-ID |
| MOM-043 | global-config-clash | Modules must use namespaced configuration keys | error | Use of generic config keys like db.url instead of modules.ordering.db.url |
| MOM-044 | feature-flag-bypass | New modules must be toggleable via feature flags | warning | Module registration code that cannot be disabled without a code change |
| MOM-045 | dead-module | Unused modules should be removed or archived | warning | Module directories with no incoming references or entry points |
| MOM-046 | bypass-validation | Cross-module calls must not bypass input validation | error | Module B calling Module A's API with raw, unvalidated data structures |
| MOM-047 | monolithic-build | Build scripts must allow for incremental compilation of modules | warning | A single file change in modules/A triggering a full re-compile of all modules |
| MOM-048 | manual-migration | Database migrations must be automated per module | error | Presence of SQL scripts in modules/*/db/ not managed by a migration tool |
| MOM-049 | leaked-test-code | Test utilities must not be included in production module builds | error | import <pkg>.modules.moduleA.tests found in internal/ code |
| MOM-050 | version-drift | All modules must share the same version of platform dependencies | error | Module A using Spring v5 while Module B uses Spring v6 in the same binary |
Resilience & Fault Tolerance
The Resilience doctrine enforces patterns that ensure a system remains functional (perhaps in a degraded state) despite the inevitable failure of its components, network, or downstream dependencies. It aims to prevent cascading failures that turn a minor service hiccup into a total system outage.
Foundational Works:
Practitioner Guidance:
Anti-Patterns / Failure Cases:
| ID | Category | Rule | Severity | Scan For |
|---|---|---|---|---|
| RES-001 | missing-timeout | Every network call must have an explicit timeout | error | HTTP/gRPC/DB client calls without a timeout configuration |
| RES-002 | infinite-wait | Blocking calls must not wait indefinitely for a response | error | Usage of default "infinite" or -1 timeout values in library configs |
| RES-003 | excessive-timeout | Timeouts should not exceed the user's patience threshold | warning | Timeouts configured for > 5s on user-facing request paths |
| RES-004 | missing-deadline | Propagate deadlines across service boundaries | warning | Lack of context.WithDeadline or X-Request-Deadline propagation |
| RES-005 | static-timeout | Use dynamic timeouts based on remaining request budget | warning | Hardcoded 500ms timeout when the total request budget is variable |
| RES-006 | missing-breaker | All external service calls must be wrapped in a circuit breaker | error | Network client calls missing a breaker decorator/wrapper |
| RES-007 | misconfigured-threshold | Breakers must have a defined failure rate threshold | warning | Circuit breaker settings with failureRateThreshold > 50% |
| RES-008 | sticky-open-breaker | Breakers must define an automatic transition to half-open state | error | Lack of waitDurationInOpenState or equivalent config |
| RES-009 | missing-health-integration | Breaker state must influence service health status | warning | /health returning UP when critical path breakers are OPEN |
| RES-010 | slow-call-breaker | Circuit breakers must also trigger on slow calls, not just errors | warning | Lack of slowCallRateThreshold in breaker configurations |
| RES-011 | retry-storm | Retries must implement exponential backoff and jitter | error | Retry logic using constant intervals (e.g., sleep(1s)) without randomization |
| RES-012 | infinite-retries | Retries must have a maximum attempt limit | error | Recursive or loop-based retries without a counter (max 3 recommended) |
| RES-013 | side-effect-retry | Do not retry non-idempotent operations (POST/PATCH) | error | Retry logic wrapping non-GET/non-HEAD requests without idempotency keys |
| RES-014 | shallow-retries | Do not retry on 4xx Client Errors | warning | Retries triggering for 401 Unauthorized or 404 Not Found |
| RES-015 | hidden-retries | Avoid nested retries (Library Retry + Application Retry) | warning | Both the HTTP library and the application logic having retry policies enabled |
| RES-016 | shared-thread-pool | Distinct dependencies must use separate thread pools | warning | Multiple clients/ sharing one global thread pool/executor |
| RES-017 | missing-bulkhead | Limit concurrent calls to any single dependency | error | Lack of semaphores or pool size limits on outgoing client calls |
| RES-018 | resource-exhaustion | Resilience wrappers must be registered as singletons | warning | Dynamic creation of breakers/retries per-request causing memory leaks |
| RES-019 | unconstrained-queues | Bulkhead queues must have a maximum capacity | error | Usage of unbounded queues (e.g., LinkedBlockingQueue without size) |
| RES-020 | shared-circuit-breaker | Do not share a single circuit breaker across different service endpoints | error | One breaker instance guarding multiple unrelated external APIs |
| RES-021 | missing-fallback | Every circuit breaker must have a defined fallback action | error | Breakers without a recover or fallback method |
| RES-022 | opaque-fallback | Fallbacks should indicate that the data is degraded | warning | Fallbacks returning "empty" data without a degraded: true flag |
| RES-023 | recursion-in-fallback | Fallbacks must not trigger secondary unprotected network calls | error | A fallback method that makes its own unprotected network call |
| RES-024 | fallback-logic-leak | Do not put complex business logic in fallback methods | warning | Fallbacks exceeding 10 lines of code or performing complex calculations |
| RES-025 | fallback-failure-loop | Fallback methods must be guaranteed to succeed or fail-fast | error | Fallback logic that itself contains nested retry loops or complex IO |
| RES-026 | missing-load-shedding | Services must drop requests when overloaded | warning | Lack of an "Active Request Limit" or "Admission Control" middleware |
| RES-027 | queue-clogging | Use LIFO or Priority queues when shedding load | warning | Standard FIFO queues that keep "old" requests during a spike |
| RES-028 | expensive-health-check | Health checks must be lightweight and non-blocking | error | /health endpoints that perform deep DB queries or external API calls |
| RES-029 | startup-deadlock | Do not wait for all dependencies before starting the app | warning | Blocking the main() thread until a DB or Cache is available |
| RES-030 | missing-backpressure | Propagate backpressure signals to the caller | error | Catching overload errors and returning generic 500 instead of 503 Service Unavailable |
| RES-031 | silent-recovery | State changes in circuit breakers must be logged | warning | Breaker transitions (CLOSED -> OPEN) without a log or event |
| RES-032 | missing-chaos-test | High-traffic integration points must be chaos-tested | warning | Lack of Toxiproxy or Chaos Mesh experiments in CI/CD |
| RES-033 | invisible-retries | Retries must be incrementing a specific metric | error | Retrying without incrementing a service_retries_total counter |
| RES-034 | opaque-latency | Measure latency *with* and *without* resilience overhead | warning | Only measuring final response time, ignoring time spent in retries |
| RES-035 | missing-fallback-alert | Frequent fallback triggering must trigger an alert | warning | Lack of an alert for fallback_calls_total exceeding a threshold |
| RES-036 | cache-as-fallback-only | Do not use cache-as-fallback for sensitive real-time data | error | Returning cached "Balance" or "Stock Level" during a service outage |
| RES-037 | missing-ttl-on-fallback | Fallback data must have a strict Time-To-Live | warning | Serving fallback data that hasn't been refreshed in over 24 hours |
| RES-038 | cache-stampede-protection | Use "Singleflight" or "Coalescing" for cache misses | warning | Multiple threads hitting the same DB record simultaneously on a cache miss |
| RES-039 | hardcoded-resilience | Resilience parameters must be configurable at runtime | error | Hardcoded failureRateThreshold: 0.5 inside the source code |
| RES-040 | missing-dry-run | Large resilience changes should be deployable in "Audit" mode | warning | Lack of a force_open or dry_run flag in the breaker configuration |
| RES-041 | configuration-dependency | Resilience config must not depend on a failing remote config server | error | Service failing to start its resilience layer because it can't fetch remote config |
| RES-042 | shared-secrets-in-resilience | Resilience logs must not contain authentication headers | error | Logging the raw downstream request/response when it contains Authorization tokens |
| RES-043 | static-state-in-resilience | Resilience policies must be stateless regarding user data | error | Storing user-specific data in a shared Retry or CircuitBreaker instance |
| RES-044 | unmanaged-goroutines | Background tasks must use a lifecycle-aware pool | error | Usage of go func() or Thread.start() for background resilience tasks |
| RES-045 | context-leak | Ensure contexts are passed to all async resilience tasks | error | Start of a background task that doesn't listen for parent shutdown |
| RES-046 | blocking-event-loop | Resilience logic must not block the main event loop | error | Synchronous Thread.sleep() in an async framework (Node.js/Netty) |
| RES-047 | excessive-concurrency | Limit the number of background retries to prevent OOM | warning | Lack of a global limit on the number of concurrently running retry workers |
| RES-048 | missing-graceful-drain | Wait for resilience tasks to complete before shutdown | warning | Service exiting immediately without letting retries finish their "last attempt" |
| RES-049 | dependency-cycle-resilience | Do not create a resilience dependency on a service you are protecting | error | A circuit breaker that calls a logging service which itself is behind that same breaker |
| RES-050 | orphaned-resilience | Ensure every resilience policy is actually attached to a client | warning | Definition of a CircuitBreaker or Retry policy that is never used in code |
Configuration
.architecture/config.yml tells Inquisition which
doctrines to apply and which directories to scan. Generate it automatically with
/puritan:covenant discover.
Override severity per doctrine or per rule in the optional
.architecture/decisions.yml — set doctrines to
strict, pragmatic,
or aspirational.
# .architecture/config.yml
doctrines:
- name: ddd
enabled: true
targets:
- domain/
- application/
- name: cqrs
enabled: true
targets:
- domain/commands/
- infrastructure/projections/
layers:
domain:
- domain/
application:
- application/
exclude:
- "**/migrations/**"
- "**/*.generated.*" Typical workflow
Greenfield project
/puritan:covenant — discuss patterns, get recommendations/puritan:inquisition — run on each PR to catch violations early/puritan:scriptorium — add doctrines as new patterns are adoptedExisting codebase
/puritan:covenant discover — scan directory structure, generate config/puritan:inquisition full — full audit to establish baselineaspirational to avoid noise/puritan:scriptorium — codify informal team rules