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.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- microservice-boundaries Installs to ~/.agents/skills/microservice-boundaries/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install microservice-boundaries The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project microservice-boundaries Installs to .agents/skills/microservice-boundaries/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/microservice-boundaries.zipunzip -o microservice-boundaries.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) microservice-boundaries PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
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
- You decided to stay in one deployable and need enforced modules: use modular-monolith
- You need the step-by-step traffic migration from legacy to new: use strangler-fig-migration
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.
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?
| Signal | Split helps | Split hurts |
|---|---|---|
| Teams | 3+ teams blocked on each other’s releases | One team, or under ~15 engineers |
| Scaling | One part needs very different scale or hardware | Uniform load |
| Isolation | Failure or security domain must be separated (payments, PII) | No such need |
| Release cadence | Parts need different release rhythms | Everything ships together anyway |
| Operations maturity | CI/CD per service, tracing, on-call exist | No 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:
- Business capabilities. What the business does, as verbs and nouns: Catalogue, Pricing, Ordering, Fulfilment, Billing, Identity. Stable over years, unlike org charts.
- Bounded contexts. Where terms change meaning ($domain-driven-design): “Product” in Catalogue vs “Item” in Fulfilment.
- Team ownership. Conway’s law: one service, one owning team. A service owned by two teams has no owner.
- 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.
- 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-pattern | How to spot it | Fix |
|---|---|---|
| Distributed monolith | Services must deploy together; lockstep version bumps | Merge, or move the shared logic to one owner |
| Shared database | Two services write the same table; migrations need cross-team sign-off | Assign an owner; others migrate to its API/events |
| Entity (CRUD) services | UserService, ProductService with getters, called by everything | Re-cut by capability; replicate needed attributes |
| Chatty synchronous chain | One request fans through 4+ services serially | Move data closer (event-carried state), merge, or aggregate at a BFF |
| Nano-services | One endpoint or function per service, one team runs 30 | Merge into capability-sized services |
| Shared domain library | A common-models package all services pin | Contracts as schemas (OpenAPI, Avro), not shared code |
| Two-team ownership | Pager rotation spans teams; nobody can say no | Pick 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.yaml
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.py
#!/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.