Modular Monolith skill for Codex

Service-grade boundaries in one deployable. Codex splits a single deployable into capability modules, each with a small public API, private internals, its own schema and in-process events, then wires a boundary checker for the stack (Spring Modulith, Packwerk, Nx, import-linter, .NET projects or Go internal packages) into CI with a shrinking baseline. It gets most of what teams want from microservices without network calls, and leaves modules ready to extract later.

  • Architecture
  • $modular-monolith
  • 203 lines
  • 2 files
  • ~1,950 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- modular-monolith

Installs to ~/.agents/skills/modular-monolith/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $modular-monolith, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Structures a single deployable application into enforced modules - each with a public API, private internals, its own tables, and in-process events - and makes the boundaries fail the build when crossed, using Spring Modulith, Packwerk, Nx module boundaries, import-linter, ArchUnit or .NET project references. Use when a monolith has turned into a big ball of mud, as the default alternative to premature microservices, when preparing a module for later extraction, or when reviewing cross-module coupling in one codebase.

Use it when

  • A single codebase has tangled dependencies between features
  • The team is considering microservices without a scaling or isolation driver
  • You want to prepare a module for later extraction into a service
  • Cross-module joins or imports keep slipping through review

Use something else when

How to check the result

A throwaway import of another module's internals makes CI fail, module tests pass with neighbours faked at their APIs, and no module's SQL references another module's schema.

The mistake it prevents

Defining modules by technical layer, or exposing ORM entities through module APIs, so every caller can still lazy-load another module's tables.

~/.agents/skills/modular-monolith/SKILL.md

--- name: modular-monolith description: Structures a single deployable application into enforced modules - each with a public API, private internals, its own tables, and in-process events -… license: MIT ---

A modular monolith is working when each module can be understood, tested and changed through its public API alone, no module reads another module’s tables, and a boundary violation fails CI rather than waiting for a code review to notice. It keeps one deploy, one process and local calls, and buys most of the team-independence benefits of services.

When to choose it

SituationChoose
One to a few teams, product still finding its shapeModular monolith
Monolith with tangled code but no scaling or isolation driverModularise in place
Planning microservices “eventually”Modular monolith first; extract modules that prove they need it
A part needs separate scaling, runtime, security isolation or release cadence nowExtract that module ($microservice-boundaries)

What makes a module

PropertyRule
CapabilityNamed for a business capability (orders, billing, catalog), not a layer
Public APIA small facade, service interface or package other modules may call; everything else is private
DataOwns its tables (own schema or table prefix); no other module reads or writes them
EventsPublishes domain events other modules subscribe to, in process
TestsHas tests that run against its API with other modules stubbed
OwnerOne team in CODEOWNERS

Layout

src/
  modules/
    orders/
      api/             # public: OrdersApi, DTOs, published events
      internal/        # private: domain, persistence, handlers
      migrations/      # tables owned by orders (schema "orders")
    billing/
      api/
      internal/
    catalog/
  shared/              # tiny: Money, ids, clock, logging - no business logic
  app/                 # composition root, HTTP routing, wiring

Cross-module calls go only to modules/<other>/api. Shared code stays small and boring; a growing shared/ is a module that has not been named yet.

Enforce the boundaries

Pick the tool for the stack; run it in CI on every PR.

Java / Kotlin: Spring Modulith

Modules are the direct sub-packages of the main application package; types in nested packages are internal unless exposed.

class ModularityTests {
  ApplicationModules modules = ApplicationModules.of(ShopApplication.class);

  @Test
  void verifiesModuleStructure() {
    modules.verify();   // fails on cycles and on access to another module's internals
  }

  @Test
  void writesDocumentation() {
    new Documenter(modules).writeDocumentation();  // C4-style component diagrams per module
  }
}

Use @ApplicationModuleListener for cross-module event handlers (async, transactional), and the event publication registry (spring-modulith-starter-jpa or -jdbc) so events survive crashes. @ApplicationModuleTest boots one module in isolation.

Ruby on Rails: Packwerk

# packs/orders/package.yml
enforce_dependencies: true
enforce_privacy: true         # privacy checker comes from the packwerk-extensions gem in Packwerk 3
dependencies:
  - packs/catalog
bin/packwerk check            # fails on undeclared dependencies and private constant access
bin/packwerk update-todo      # records existing violations in package_todo.yml as a baseline

TypeScript monorepo: Nx

"@nx/enforce-module-boundaries": ["error", {
  "depConstraints": [
    { "sourceTag": "scope:orders",  "onlyDependOnLibsWithTags": ["scope:orders", "scope:catalog-api", "scope:shared"] },
    { "sourceTag": "scope:billing", "onlyDependOnLibsWithTags": ["scope:billing", "scope:orders-api", "scope:shared"] }
  ]
}]

Without Nx, use dependency-cruiser rules that forbid ^src/modules/([^/]+)/internal/ from any path outside that same module.

Python: import-linter

[importlinter:contract:orders-private]
name = Only orders may import orders internals
type = forbidden
source_modules =
    app.modules.billing
    app.modules.catalog
forbidden_modules =
    app.modules.orders.internal

Add one contract per module (or generate them), plus an acyclic check of module dependencies if your import-linter version provides it; otherwise check cycles in review.

.NET and Go

  • .NET: one project per module plus a .Contracts project as its public API; other modules reference only .Contracts. NetArchTest for rules inside a project.
  • Go: put module internals under internal/ inside the module directory; the compiler forbids imports from outside the parent.

Data ownership

  1. Give each module its own schema (orders.*, billing.*) or a strict table prefix.
  2. Use a database role per module where practical, so a stray query fails at runtime.
  3. Remove cross-module foreign keys over time; keep IDs, validate through the API.
  4. Replace cross-module joins with: an API call returning what is needed, a read model the consuming module maintains from events, or a dedicated reporting query module that is explicitly allowed to read everything (and never writes).

Find cross-module SQL quickly:

rg -n "FROM\s+(billing|catalog)\.|JOIN\s+(billing|catalog)\." src/modules/orders

Communication between modules

NeedUse
Query another module’s data nowCall its public API (synchronous, in process)
React to something that happenedIn-process domain event, handled after commit
Several modules must change together atomicallySingle transaction is allowed in a monolith, but treat it as a smell; prefer one module owning the invariant
Prepare for later extractionEvents via an outbox table so moving to a broker is a transport change

Keep event handlers idempotent from day one; extraction then needs no rework.

Workflow (modularising an existing monolith)

- [ ] 1. Read AGENTS.md; map current packages and the worst coupling hotspots
- [ ] 2. Name 4-8 candidate modules by capability; agree with the user
- [ ] 3. Move code into module folders without behaviour change (one PR per module)
- [ ] 4. Add the boundary tool with a baseline of existing violations
- [ ] 5. Define each module's public API; route callers through it; shrink the baseline
- [ ] 6. Separate data ownership: schemas, then remove cross-module joins
- [ ] 7. Turn on "no new violations" in CI, then "zero violations" per module
- [ ] 8. Document modules and owners (CODEOWNERS, AGENTS.md)

Gotchas

  • Modules by technical layer (controllers, services, models) are not modules; every feature crosses all of them.
  • A public API that returns internal entities (ORM objects) leaks the internals and lets callers lazy-load another module’s tables. Return DTOs or read-only views.
  • The baseline file must only shrink. Review any PR that adds to package_todo.yml or an equivalent ignore list.
  • Circular module dependencies hide in events too: if A listens to B and B calls A’s API on that event, document it or invert one direction.
  • A single shared transaction across modules makes later extraction a redesign. Mark each such place with a comment and track it.

Verification

  • The boundary check fails when you add an import of another module’s internals (prove once with a throwaway change, then revert).
  • Each module’s tests run with other modules stubbed or faked at their API.
  • No SQL in a module references another module’s schema (grep above returns nothing).
  • The violation baseline is empty, or shrinking with a dated plan.

Output contract

**Modules:** orders, billing, catalog, identity (owners in CODEOWNERS)
**Enforcement:** <tool>, config <path>, CI step <name>; baseline violations: <n> (was <m>)
**Data:** schemas per module: yes/partial; cross-module joins remaining: <list>
**Integration:** public APIs at modules/*/api; events: <list>, outbox: yes/no
**Next extraction candidate (if any):** <module> because <driver>

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Modular Monolith"
  short_description: "Service-grade boundaries in one deployable"
  default_prompt: "$modular-monolith Our monolith is a big ball of mud - split it into enforced modules without going to microservices"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem