SPB Git

spb/ultra-sharp-agent-skills Public

Ultra-Sharp Agent Skills — a research-first skill-authoring system + 72 production-ready skills for AI agents.

Python 100%

# Patterns — Architecting Service Boundaries

# Contents

  • Split/don't-split decision table
  • Modular monolith enforcement
  • Saga with compensations (order placement)
  • Local read model via events
  • Strangler extraction plan
  • Distributed-monolith smell test
  • Gotchas

# Split/don't-split decision table

Signal Verdict
Module needs 10× the replicas of the rest Split (scaling trigger)
Module ships weekly while the rest ships daily and blocks it Split (cadence trigger)
Two teams keep merge-conflicting in one module Split (ownership trigger)
"It would be cleaner as a service" Don't split
Junior team, no platform/on-call maturity Don't split yet
Module is called by everything, in-process, latency-sensitive Don't split

# Modular monolith enforcement

Enforce boundaries inside one deployable so extraction stays cheap:

text
app/
├── billing/        # public API: billing/api.py only
├── catalog/
├── fulfillment/
└── shared/         # pure utilities only — no business logic
  • Each module exposes one façade (billing/api.py); cross-module imports of internals fail CI (import-linter / ArchUnit / eslint-boundaries).
  • Each module gets its own schema/namespace in the database from day one — table ownership is then already settled when a split comes.

# Saga with compensations (order placement)

text
Step                    Compensation
1. reserve inventory  → release reservation
2. charge payment     → refund payment
3. create shipment    → cancel shipment
4. send confirmation  → (terminal; deliberately last — cannot be unsent)
python
SAGA = [
    (reserve_inventory, release_reservation),
    (charge_payment,    refund_payment),
    (create_shipment,   cancel_shipment),
    (send_confirmation, None),          # irreversible step goes last
]

def run_saga(order):
    done = []
    for step, compensate in SAGA:
        try:
            step(order)
            done.append(compensate)
        except StepFailed:
            for comp in reversed(done):   # unwind in reverse order
                if comp:
                    comp(order)
            raise

Orchestration (one coordinator, above) is the default — easier to trace and test. Choreography (each service reacts to events) is the escape hatch when the coordinator itself becomes a coupling point.

# Local read model via events

Instead of fulfillment calling catalog on every shipment:

text
catalog  --publishes-->  product.updated {id, weight, dims}
fulfillment --consumes--> upserts into its own product_dimensions table

Fulfillment reads locally (fast, survives catalog downtime); staleness is bounded by event lag and must be acceptable for the use case — if it isn't, the data wasn't yours to cache and the boundary may be wrong.

# Strangler extraction plan

  1. Freeze feature work in the monolith module being extracted.
  2. Stand up the new service; dual-write or replay events to fill its store.
  3. Route N% of read traffic via a routing layer flag; compare responses (shadow diff).
  4. Ramp N → 100 with the rollback switch live at every step.
  5. Cut writes over behind the same flag; verify; delete monolith code path within a sprint (a dormant duplicate path rots into rule-8 territory).

# Distributed-monolith smell test

Answer for each service; any "yes" means redraw:

  • Must it deploy in the same release train as another service?
  • Does another service read or write its tables?
  • Does one service's outage take it fully down (not degraded)?
  • Do contract changes require synchronized PRs across repos?

# Gotchas

  • Entity-shaped services (user-service that everyone calls for everything) recreate the shared database over HTTP; split by capability (identity vs profile vs preferences).
  • Shared libraries with business logic are a hidden shared schema — a change forces lockstep upgrades. Keep shared libs to pure utilities.
  • Sagas are not ACID: intermediate states are visible. Model them explicitly (PENDING_PAYMENT), don't pretend isolation.
  • Compensation ≠ undo: refund is a new transaction, not an erasure — books must show both.
  • Event schemas are contracts too — version them like APIs (rule 6), with consumers tolerant of unknown fields.
  • The routing flag is load-bearing during strangler cuts; it needs tests and an owner like any production code.