Microservice Boundaries skill for Codex

Cut services where change and data actually split. Codex checks for a real driver before recommending a split, then finds candidate boundaries where business capabilities, team ownership, data affinity and git change coupling agree. A bundled script measures how often modules change together. Every dataset gets one writing service, and shared databases, entity services and long synchronous chains are flagged with concrete fixes and an incremental extraction plan.

  • Architecture
  • $microservice-boundaries
  • 143 lines
  • 3 files
  • ~1,900 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- microservice-boundaries

Installs to ~/.agents/skills/microservice-boundaries/ 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 $microservice-boundaries, 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

Decides whether and where to split a system into microservices - finds candidate boundaries from business capabilities, bounded contexts, team ownership and git change coupling, assigns each dataset a single owning service, and flags distributed-monolith anti-patterns such as shared databases, entity services and chatty synchronous chains. Use when planning to break up a monolith, reviewing an existing service map, merging services that always change together, or deciding which service owns a table or API.

Use it when

  • Planning to decompose a monolith into services
  • Services must be released in lockstep or share database tables
  • Deciding which service owns a dataset or an API
  • Reviewing a service map for entity services and chatty call chains

Use something else when

How to check the result

Most recent or planned features touch a single service, every table has exactly one writing service, and services deploy independently in CI.

The mistake it prevents

Splitting by entity or technical layer, which produces services that all change together and call each other on every request: a distributed monolith.

~/.agents/skills/microservice-boundaries/SKILL.md

--- name: microservice-boundaries description: Decides whether and where to split a system into microservices - finds candidate boundaries from business capabilities, bounded contexts, team… license: MIT ---

A good boundary lets one team change, test, deploy and scale its service without coordinating with another team for the common case, and each piece of data has exactly one service allowed to write it. If most features still touch three services, the boundaries are wrong, no matter how clean the diagrams look.

First question: should this be split at all?

SignalSplit helpsSplit hurts
Teams3+ teams blocked on each other’s releasesOne team, or under ~15 engineers
ScalingOne part needs very different scale or hardwareUniform load
IsolationFailure or security domain must be separated (payments, PII)No such need
Release cadenceParts need different release rhythmsEverything ships together anyway
Operations maturityCI/CD per service, tracing, on-call existNo observability, manual deploys

If the drivers are code tidiness or “modern architecture”, recommend a modular monolith with enforced boundaries ($modular-monolith) and revisit when a real driver appears. Extracting services out of a well-modularised monolith later is cheap; merging badly-cut services is expensive.

Finding candidate boundaries

Use several lenses and look for where they agree:

  1. Business capabilities. What the business does, as verbs and nouns: Catalogue, Pricing, Ordering, Fulfilment, Billing, Identity. Stable over years, unlike org charts.
  2. Bounded contexts. Where terms change meaning ($domain-driven-design): “Product” in Catalogue vs “Item” in Fulfilment.
  3. Team ownership. Conway’s law: one service, one owning team. A service owned by two teams has no owner.
  4. Change coupling. Modules that change in the same commits belong together:
python3 scripts/change_coupling.py --repo . --prefix src/ --depth 1 --since "12 months ago"
python3 scripts/change_coupling.py --repo . --prefix app/domains/ --min-commits 10 --top 40

The script reads git log (offline, standard library only) and prints module pairs with how often they change together. Above roughly 30% co-change in both directions, treat the pair as one unit or redesign the interface between them before splitting.

  1. Data affinity. Tables joined in the same queries and updated in the same transactions should usually stay together:
rg -n "JOIN\s+\w+" --glob '*.{sql,py,rb,ts,java,go}' | head -50

Rules for a boundary

  • Single writer per dataset. One service owns each table/collection and is the only writer. Others get data through its API, its events, or a replicated read model.
  • No shared database schemas across services. A shared database server with separate schemas and credentials per service is acceptable as an interim step.
  • Autonomy on the hot path. A user request should be served by one service plus data it already holds. Count synchronous hops on the top journeys; more than two is a smell.
  • Consistency inside, eventual across. Invariants that need a single transaction must live in one service. Cross-service consistency goes through sagas ($saga-pattern) and reliable events ($outbox-pattern).
  • Capability, not entity. “Customer service” that every other service calls for CRUD is an entity service and becomes a bottleneck. Split by what is done with customers (Identity, Loyalty, Credit), each holding the attributes it needs.

Anti-patterns to flag

Anti-patternHow to spot itFix
Distributed monolithServices must deploy together; lockstep version bumpsMerge, or move the shared logic to one owner
Shared databaseTwo services write the same table; migrations need cross-team sign-offAssign an owner; others migrate to its API/events
Entity (CRUD) servicesUserService, ProductService with getters, called by everythingRe-cut by capability; replicate needed attributes
Chatty synchronous chainOne request fans through 4+ services seriallyMove data closer (event-carried state), merge, or aggregate at a BFF
Nano-servicesOne endpoint or function per service, one team runs 30Merge into capability-sized services
Shared domain libraryA common-models package all services pinContracts as schemas (OpenAPI, Avro), not shared code
Two-team ownershipPager rotation spans teams; nobody can say noPick one owner

Splitting procedure (from a monolith)

- [ ] 1. Confirm a real driver (table above) and record an ADR
- [ ] 2. Choose the first candidate: clear capability, low change coupling, few inbound calls
- [ ] 3. Make it a module inside the monolith first; enforce its boundary
- [ ] 4. Give the module its own schema/tables; remove foreign keys and joins from outside
- [ ] 5. Put an interface (in-process API) in front of it; route all callers through it
- [ ] 6. Extract to a service behind the same interface; strangle callers over (strangler fig)
- [ ] 7. Migrate data ownership; other parts read via API/events, never the tables
- [ ] 8. Measure: deploys per week, cross-service changes per feature, p99 latency

Steps 3-5 deliver most of the value and are reversible. Step 6 onwards is where network, deployment and consistency costs arrive. For incremental routing patterns during step 6, see $strangler-fig-migration.

Data migration needs care: cutovers, backfills and dual-read periods can lose or duplicate data. Show the plan and get explicit user confirmation before any step that moves or deletes production data.

Sizing guidance

  • A service should be ownable by one team and understandable by one engineer in a few days.
  • Prefer fewer, larger services at first. Splitting later along a seam that proved itself is easier than merging.
  • Count the operational overhead per service: CI pipeline, dashboards, alerts, on-call runbook, dependency updates. If the team cannot afford that per service, there are too many.

Gotchas

  • Splitting by technical layer (frontend service, backend service, database service) is not a service boundary; every feature then touches all of them.
  • Reporting and search need data from everywhere. Feed them from events or CDC into a dedicated read store instead of letting them query every service.
  • Moving a foreign key across a service boundary moves referential integrity into application code. Decide what happens when the referenced entity is deleted.
  • Authorisation logic duplicated across services drifts. Centralise policy decisions (a policy service or library with a single source) even if enforcement is distributed.
  • Latency budget: every hop adds network time and failure probability; check the top journey’s p99 against the SLO before and after.

Verification

  • For the last 20 features (or the planned ones), count how many services each touches. Most should touch one.
  • Every table has exactly one writing service (grep connection strings and migrations).
  • Change-coupling report after the split shows low co-change across service boundaries.
  • Each service deploys independently in CI without coordinated releases.

Output contract

**Recommendation:** split now | modular monolith first | merge services X and Y
**Drivers:** <the real reasons>
**Proposed services:** | Service | Capability | Owning team | Data it owns | Sync deps |
**Evidence:** change-coupling top pairs, shared tables found, hop count on top journeys
**Anti-patterns found:** <list with fix>
**First extraction:** <candidate>, steps, rollback point, data migration needing sign-off

What the zip contains

Codex reads bundled files only when a step points to them. Every one is shown here, so you can review it before installing.

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: "Microservice Boundaries"
  short_description: "Cut services where change and data actually split"
  default_prompt: "$microservice-boundaries We want to break our Rails monolith into services - where should the first cut be?"
scripts/change_coupling.py85 lines, 3.4 KB
#!/usr/bin/env python3
"""Measure how often modules change together in git history (change coupling).

Groups each changed file into a module by its leading path segments, then counts, for
every pair of modules, the commits that touched both. High co-change between two
candidate services means they are not independent.

Usage:
  python3 change_coupling.py [--repo .] [--depth 1] [--since "12 months ago"]
                             [--prefix src/] [--min-commits 5] [--top 25]

Standard library only. Requires git on PATH. Exits 1 on error.
"""
from __future__ import annotations

import argparse
import itertools
import subprocess
import sys
from collections import Counter

MAX_FILES_PER_COMMIT = 200  # skip mass reformatting / vendoring commits


def module_of(path: str, prefix: str, depth: int) -> str | None:
    if prefix and not path.startswith(prefix):
        return None
    rest = path[len(prefix):] if prefix else path
    parts = rest.split("/")
    if len(parts) <= 1:  # file directly under prefix: not inside a module
        return None
    return "/".join(parts[: min(depth, len(parts) - 1)])


def main() -> int:
    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("--repo", default=".")
    ap.add_argument("--depth", type=int, default=1, help="path segments after prefix that name a module")
    ap.add_argument("--since", default="12 months ago")
    ap.add_argument("--prefix", default="", help="only consider files under this path, e.g. src/")
    ap.add_argument("--min-commits", type=int, default=5, help="ignore modules with fewer commits")
    ap.add_argument("--top", type=int, default=25)
    args = ap.parse_args()

    cmd = ["git", "-C", args.repo, "log", "--no-merges", f"--since={args.since}",
           "--name-only", "--pretty=format:--commit--"]
    try:
        out = subprocess.run(cmd, check=True, capture_output=True, text=True).stdout
    except (OSError, subprocess.CalledProcessError) as exc:
        print(f"error: git log failed: {exc}", file=sys.stderr)
        return 1

    commits: list[set[str]] = []
    for block in out.split("--commit--"):
        files = [f for f in block.strip().splitlines() if f.strip()]
        if not files or len(files) > MAX_FILES_PER_COMMIT:
            continue
        mods = {m for f in files if (m := module_of(f, args.prefix, args.depth))}
        if mods:
            commits.append(mods)

    if not commits:
        print("no commits matched; check --prefix, --depth and --since", file=sys.stderr)
        return 1

    per_module = Counter(m for c in commits for m in c)
    keep = {m for m, n in per_module.items() if n >= args.min_commits}
    pairs: Counter[tuple[str, str]] = Counter()
    for mods in commits:
        for a, b in itertools.combinations(sorted(mods & keep), 2):
            pairs[(a, b)] += 1

    print(f"commits analysed: {len(commits)}  modules: {len(keep)}  since: {args.since}\n")
    print(f"{'module A':<32} {'module B':<32} {'together':>8} {'A%':>5} {'B%':>5}")
    rows = sorted(pairs.items(), key=lambda kv: kv[1] / min(per_module[kv[0][0]], per_module[kv[0][1]]), reverse=True)
    for (a, b), n in rows[: args.top]:
        pa = 100 * n / per_module[a]
        pb = 100 * n / per_module[b]
        print(f"{a:<32} {b:<32} {n:>8} {pa:>4.0f}% {pb:>4.0f}%")
    print("\nA% = share of A's commits that also touched B. Above ~30% both ways: treat as one unit.")
    return 0


if __name__ == "__main__":
    sys.exit(main())

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
New

Introducing Codex Skills Pro

79 professional skills that take Codex through complete engagements, each with its own scripts, references and a report that verifies the result.

All-AccessOne-time payment, no subscription
$49
Get All-AccessOr browse individual skills from $2.99 →

Secure checkout by Creem