agentleFS
G1JoshiG1Joshi/Agent-Skills/skills/architecture/modular-monolith/SKILL.md

modular-monolith

Skill15 starsChanged 43 days agoMITView on GitHub

Expert Modular Monolith architecture assistance covering bounded module boundaries, internal public APIs, decoupled package structures, and single-deployment velocity. Use when scaling monolith development teams, preventing spaghetti code, or preparing for future microservice extraction.

The file

We split this skill into the rules it gives an agent

1 rule. About 0 are common, 1 is distinctive, and 0 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈1

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 0

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Keep Deployment Pipeline Unified: Enjoy the speed of atomic single-binary deployments and coordinated database migrations.

    line 137

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Modular Monolith

A Modular Monolith structures a single deployment artifact into discrete, loosely-coupled domain modules with strict encapsulation boundaries, balancing microservice-level separation with monolithic operational simplicity.

When to Use

  • Medium-to-Large Engineering Teams: Scaling a codebase across multiple teams without the operational overhead of microservices.
  • Single-Deployment Velocity: Retaining simple CI/CD pipelines, single-transaction database migrations, and local Docker setups.
  • Clear Domain Boundaries: Preventing spaghetti code by enforcing strict module encapsulation and public API boundaries at compile-time.
  • Pre-Microservice Preparation: Architecting systems that can be cleanly sliced into separate microservices later if scaling demands it.

Quick Start

/src
  /Modules
    /Catalog       <-- Public Interface defined here
      /Core        <-- Internals (Classes, Logic) hidden
      /API         <-- Public Contracts (DTOs)
    /Ordering
      /Core
    /Shipping
      /Core
  /Shared          <-- Infrastructure, Event Bus
// Communication via In-Process Interfaces or Events
public class OrderService {
    private readonly ICatalogModule _catalog; // In-memory reference, but strict contract

    public async Task Checkout(string productId) {
        var product = await _catalog.GetProduct(productId); // Fast 0ms call
        // ...
    }
}

Core Concepts

Strict Module Encapsulation & Public API Facades

Modules interact exclusively through explicit public facades; internal repositories, models, and helpers are unexported:

src/modules/
  ├── billing/
  │   ├── internal/        # Private tables, services, entities
  │   └── index.ts         # Public Interface & Facade ONLY
  └── shipping/
      ├── internal/
      └── index.ts

In-Process Domain Events

Modules communicate asynchronously across boundaries using in-memory event buses:

// modules/common/event-bus.ts
import EventEmitter from "events";
export const inProcessBus = new EventEmitter();

// modules/orders/internal/order.service.ts
inProcessBus.emit("order.created", { orderId: "123", customerId: "456" });

// modules/notifications/internal/notification.listener.ts
inProcessBus.on("order.created", (payload) => {
  // Executes in same process, decoupled from order module code
});

Architecture Linter Boundary Enforcement

Enforces boundary rules in CI using tools like eslint-plugin-boundaries or ArchUnit:

// .eslintrc.js
rules: {
  "boundaries/element-types": [2, {
    default: "disallow",
    rules: [
      { from: "module:billing", allow: ["module:billing", "module:common"] },
      { from: "module:shipping", allow: ["module:shipping", "module:common"] },
    ]
  }]
}

Architectural Advantages over Microservices

  • Zero-Latency In-Memory Invocation: Module boundaries communicate through typed facades without network serialization or RPC latency.
  • Local Transactional Consistency: ACID transactions remain possible across co-located tables without distributed 2PC or complex Saga orchestration.
  • Unified Toolchain Refactoring: IDE refactorings, cross-cutting interface changes, and atomic commits span the full system without multi-repo drift.

Common Patterns

Internal Module Public API Facade

Problem: Modules access each other's private tables, destroying encapsulation and preventing future service extraction.
Solution: Expose an explicit Public API interface per module.

// modules/billing/index.ts (Billing Module Public Boundary)
export interface BillingModuleApi {
  chargeCustomer(
    customerId: string,
    amountCents: number,
  ): Promise<PaymentReceipt>;
  getSubscriptionStatus(customerId: string): Promise<SubscriptionStatus>;
}

// Internal billing tables, services, and repositories are NOT exported.
export class BillingFacade implements BillingModuleApi {
  constructor(private readonly paymentService: InternalPaymentService) {}
  async chargeCustomer(id: string, amount: number) {
    return this.paymentService.processCharge(id, amount);
  }
  async getSubscriptionStatus(id: string) {
    return this.paymentService.checkSubscription(id);
  }
}

Best Practices

Do:

  • Enforce Boundaries with Build Tools: Use ESLint rules, TypeScript project references, or ArchUnit to prevent illegal module cross-imports.
  • Isolate Module Schemas: Use separate database schemas (billing.*, users.*) within the same database to prevent illicit SQL joins.
  • Communicate Across Modules via Facades or Events: Disallow direct access to another module's internal classes or database repositories.
  • Keep Deployment Pipeline Unified: Enjoy the speed of atomic single-binary deployments and coordinated database migrations.

Don't:

  • Execute direct cross-module foreign key joins: Reference entities from foreign modules by ID only; do not write LEFT JOIN shipping.parcels.
  • Share mutable memory state across modules: Pass immutable DTOs or primitive values through public facade methods.
  • Prematurely split into microservices: Extract a module into a microservice only when deployment frequency or scaling demands it.

Troubleshooting

ErrorCauseSolution
Cyclic module dependencyModules directly importing internal classes across boundaries.Expose strictly typed public APIs/facades and enforce boundary linting.
Accidental shared database tableCross-module queries joining tables across domains.Isolate schemas per module or access data exclusively through module interfaces.
Leaky module abstractionsDirect access to internal database entities instead of DTOs.Return immutable Data Transfer Objects (DTOs) from module public services.

References

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.