As engineering organizations scale past 50 developers, monorepos often shift from an accelerator to a source of friction. Without automated architectural guardrails, circular dependencies multiply, shared libraries become dumping grounds, and refactoring becomes too risky to attempt.
If architectural constraints exist only in a Notion wiki or a senior engineer's head, they do not exist. They must be validated automatically on every commit.
The Monorepo Decay Cycle
The fatal pattern in shared repositories is the "convenience import": a frontend dashboard module importing a backend database schema directly because they share the same repository. Over time, internal boundaries dissolve until the entire application becomes an entangled monolith.
Enforcing Package Boundaries
Modern clean code practices require explicit interface contracts. Internal implementation details within packages/core should never be exposed to consumers. GitRabbit enforces strict visibility levels across package workspaces.
version: "2.0"
architecture_rules:
- name: "enforce-domain-boundary"
description: "Prevent UI components from directly importing database models"
severity: "error"
source_patterns: ["apps/web/**/*.tsx"]
forbidden_imports:
- "@acme/database"
- "@acme/server-core"
allowed_alternative: "Use domain service contracts via @acme/api-client"
- name: "no-leaky-abstractions"
description: "Ensure storage providers are injected via interfaces"
source_patterns: ["packages/services/**/*.ts"]
prohibit_direct_instantiation: ["PrismaClient", "RedisClient"]
Defining Rules with gitrabbit.yaml
The advantage of declarative architecture rules is that they act as automated peer reviewers. When a developer submits a PR with a forbidden import, GitRabbit flags it in seconds, offering the canonical architectural alternative.
Refactoring Legacy Modules to Clean Architecture
Let's examine how clean hexagonal architecture (ports and adapters) decouples business logic from external dependencies:
// Clean Port (Interface) Definition
export interface PaymentProcessorPort {
charge(amountCents: number, currency: string, token: string): Promise<PaymentResult>;
}
// Business Domain Service - Pure Logic, zero direct third-party SDK imports
export class CheckoutService {
constructor(
private readonly paymentGateway: PaymentProcessorPort,
private readonly auditLogger: AuditLoggerPort
) {}
async processOrder(order: Order, token: string): Promise<Receipt> {
const payment = await this.paymentGateway.charge(order.total, order.currency, token);
if (!payment.success) {
throw new PaymentDeclinedError(payment.reason);
}
await this.auditLogger.record({ orderId: order.id, status: "PAID" });
return new Receipt(order, payment.transactionId);
}
}
Continuous Architectural Linting in CI
By wiring architectural checks into your CI pipeline, teams maintain high velocity without incurring technical debt. When refactoring legacy code, you can use GitRabbit's --fix flag to automatically generate adapter interfaces for older modules.


