Journal
AI 16 min read

Notes on Claude Code and Operational Memory in Microservices

A context layer built on top of Claude Code for a polyglot microservices platform: three layers of static knowledge, a six-step pipeline, and what happened when it did something I didn't ask it to.

After working for years as a backend developer on monolithic systems, moving to a polyglot microservice architecture was quite a learning curve. Monolithic applications were more complex, more business rules, but you get used to it because you touch enough files, write enough queries and logic that your brain forms a good map of it over time. But my problem with microservices was that I hardly got to work on any one codebase enough times to get used to it. It was always working on one codebase at a time for days, then switching to another one. You see the previous one again after weeks if not months. The same goes for the infrastructure, resources, and information.

If it’s so scattered, how do I connect the dots in my mind? I eventually will, probably, but it will take a good amount of time and effort. And if we face this, our AI does as well. The difference is that it is really quick at grasping things, but still burns context on multi-repo scans nonetheless.

So I thought of collecting this fragmented information from various sources and putting it together for my agent. I started with a small folder which holds 3 files — platform architecture, flows, topology, domain rules, infra, config.

While development most of my context switching was happening to gather data from:

  1. AWS Console (ex, check the EventBridge scheduled time)
  2. Helm (ex, fetch the name of S3 bucket configured in x service?)
  3. API Specs (ex, which service exposes this endpoint?)
  4. DevOps config files (ex, what are the client_id/secret configurations for x service)
  5. Github + Jira (ex, task details, figma, etc)
  6. Confluence design and architectural docs (ex, who calls x service, where does data of y come from?)

Again, fragmentation. We now use enough MCPs to connect the applications. But it is a lot to scan and find without knowing where to look precisely.

But as developers who already have most of the services and Step Functions, Lambdas and other repositories cloned, I thought: why leave the workspace or the CLI session I was already so immersed in?

So here is a simple design I have used — a context layer I built, a small set of curated files, a handful of skills that read them, and one inversion of how most AI coding tools work that I think matters.


The Core Idea

For application-level questions, the curated context files are the primary source, and the codebase is the fallback.

Teach the agent to navigate your workspace and arrange the workspace in a way your agent understands it. It sees the clear picture in one frame.

It is a workspace architecture — a set of conventions, curated files, and skills layered on top of an existing multi-repo workspace. The AI agent navigates the workspace on its own instead of relying on the developer to feed it context.

Three layers compose the system:

Raw workspace (flat folder of repositories)

    + Routing map         ← tells the agent where to look for what

    + Context files       ← pre-indexed platform knowledge

    + Work history        ← auto-recorded via git hook

    ──────────────────────────────────────────────────────

    = Agent with persistent platform understanding

The core idea is different: curate the fragmented information that already exists across the platform into a small number of high-signal files that act as the primary source of truth for the agent.


Workspace Layout

All repositories sit flat in a single root directory. The context layer adds three things to that structure:

$WORKSPACE_ROOT/
  ├── order-service-*/        # service repo
  ├── api-specs/              # OpenAPI specs
  ├── helm-charts/            # Helm configs
  ├── infrastructure/         # IaC modules

  ├── .context/               # context layer
  │   ├── system_map.yaml     # topology & blast radius
  │   ├── flows.yaml          # end-to-end call chains
  │   └── domain_rules.yaml   # rules & contracts

  ├── .tasks/
  │   └── Task-1234/
  │       ├── context.md
  │       ├── analysis.md
  │       └── plan.md

  ├── .claude/
  │   ├── CLAUDE.md           # routing map
  │   └── commands/           # slash commands

  └── tracker.db              # work history

Layer 1 — The Routing Map

The .context/ directory is the heart of the system. The routing map (CLAUDE.md) is what makes it navigable without manual loading.

CLAUDE.md is loaded automatically at the start of every agent session. It does one thing: tell the agent where to look for what. It does not contain platform knowledge itself — it points to where that knowledge lives.

A well-structured routing map has three parts: an answer-priority rule (code first, context second), a routing table, and a workspace layout reference.

## Answer Priority

1. Code-level question about a specific repo → read the actual code first.
   Do not reach for context files. The code is the source of truth.

2. Cross-service or architectural question → read the relevant context file.

3. General platform question → read context files.

## Context Files — When to Read

| File                          | Read when                        |
|-------------------------------|----------------------------------|
| .context/system_map.yaml      | Callers, blast radius, topology  |
| .context/flows.yaml           | End-to-end flows, stack traces   |
| .context/domain_rules.yaml    | Data rules, auth, contracts      |
| .context/coding_patterns.yaml | Class and service structure      |

## Routing Examples

- "What database does order-service use?"    → system_map.yaml
- "Who calls the shipment API?"              → system_map.yaml
- "What breaks if carrier-integration down?" → system_map.yaml
- "How does the order placement flow work?"  → flows.yaml
- "Stack trace from notification-service"    → flows.yaml
- "Data residency rules for PII?"           → domain_rules.yaml
- "How should I structure this new service?" → coding_patterns.yaml

## Workspace Layout

All repos are flat in $WORKSPACE_ROOT.

API specs:   ./api-specs/
Helm charts: ./helm-charts/
Context:     ./.context/
Task files:  ./.tasks/<TASK-ID>/
Work DB:     ./tracker.db

The routing map is intentionally minimal. Its job is to prevent the agent from scanning six repos to answer a question that is already indexed in a 20KB YAML file.


Layer 2 — Context Files

The context files are curated YAML documents maintained by the team and refreshed on a schedule. They encode what a senior engineer would know about the platform without having to read every repository.

There are four files. Each has a distinct scope.

system_map.yaml

services:

  order-service:
    description: Creates and manages customer orders through their lifecycle
    owner: fulfillment-team
    tech: Java 21 / Spring Boot 3.x
    database: PostgreSQL (orders schema)
    sync_communication:
      calls:
        - inventory-service: check stock availability before confirming order
        - billing-service: initiate payment on order confirmation
      called_by:
        - customer-portal: place order, retrieve order status
        - carrier-integration: push delivery status updates back to orders
    async_communication:
      produces:
        - topic: order.created
          consumers: [shipment-tracking, notification-service, warehouse-management]
          schema: avro/order-created-v2.avsc
        - topic: order.cancelled
          consumers: [inventory-service, billing-service, notification-service]
          schema: avro/order-cancelled-v1.avsc
      consumes:
        - topic: payment.confirmed
          producer: billing-service
        - topic: stock.reserved
          producer: inventory-service
    openapi: api-specs/internal/order-service.yaml
    helm: helm-charts/charts/order-service
    co_deployment_group: fulfillment-core
    external_consumers: []
    if_down:
      - customer-portal: cannot place new orders
      - carrier-integration: delivery updates queue but do not persist until recovery

  shipment-tracking:
    description: Tracks shipment lifecycle from warehouse dispatch to delivery
    owner: logistics-team
    tech: Java 21 / Spring Boot 3.x
    database: PostgreSQL (shipments schema)
    sync_communication:
      calls:
        - carrier-integration: fetch live carrier status updates
        - warehouse-management: confirm dispatch readiness
      called_by:
        - customer-portal: display shipment status to customers
        - order-service: correlate delivery events with orders
    async_communication:
      produces:
        - topic: shipment.dispatched
          consumers: [notification-service, order-service]
        - topic: shipment.delivered
          consumers: [notification-service, order-service, billing-service]
      consumes:
        - topic: order.created
          producer: order-service
    openapi: api-specs/internal/shipment-tracking.yaml
    helm: helm-charts/charts/shipment-tracking
    co_deployment_group: logistics-core
    external_consumers:
      - name: partner-portal
        consumes: GET /shipments/{id}/status
        notes: External partners poll this endpoint. Breaking changes require 90-day notice.

flows.yaml

End-to-end call chains for named platform flows, with failure modes at each step. The primary file for debugging, incident triage, and understanding how features work across service boundaries.

flows:

  - name: order-placement
    description: A customer places an order through the portal
    trigger: POST /orders from customer-portal
    steps:

      - step: 1
        service: customer-portal
        action: Validates cart contents and submits order payload
        notes: Checks item availability display (stale — actual reservation happens in order-service)

      - step: 2
        service: order-service
        action: createOrder() — validates items, calls inventory and billing
        calls:
          - inventory-service: reserveStock(items)
          - billing-service: initiatePayment(amount, customerId)
        notes: Both calls are synchronous. Failure in either rolls back order creation.

      - step: 3
        service: billing-service
        action: processPayment() — charges card via payment gateway
        notes: Issues pre-auth on card. Final capture triggered by shipment.delivered event.

      - step: 4
        service: order-service
        action: Persists confirmed order, emits order.created to SNS

      - step: 5
        service: shipment-tracking
        action: Consumes order.created, creates shipment record, assigns to warehouse
        notes: Async — runs after order confirmation response returns to customer

      - step: 6
        service: warehouse-management
        action: Consumes order.created, creates pick list for warehouse staff

      - step: 7
        service: notification-service
        action: Consumes order.created, sends order confirmation email/SMS

    failure_modes:

      - step: 2
        failure: inventory-service timeout
        impact: Order rejected. Customer sees "item unavailable". No charge.
        investigation: Check inventory-service logs for DB connection saturation.

      - step: 3
        failure: payment gateway timeout
        impact: Order left in PENDING_PAYMENT. billing-service retries with exponential backoff.
        investigation: Check Stripe dashboard for pending auth. Check billing-service DLQ.

      - step: 4
        failure: SNS publish failure
        impact: Order confirmed to customer but downstream services (shipment, warehouse, notification) never notified.
        investigation: Check SNS dead-letter queue. Manual re-publish may be required.

  - name: shipment-delivery
    description: A carrier marks a shipment as delivered
    trigger: Webhook from carrier via carrier-integration
    steps:

      - step: 1
        service: carrier-integration
        action: Receives delivery webhook, validates carrier signature
        notes: HMAC validation against per-carrier secret stored in Secrets Manager

      - step: 2
        service: carrier-integration
        action: Translates carrier payload to internal shipment event format, emits shipment.delivered

      - step: 3
        service: shipment-tracking
        action: Consumes shipment.delivered, updates shipment status to DELIVERED

      - step: 4
        service: order-service
        action: Consumes shipment.delivered, updates order status to COMPLETED

      - step: 5
        service: billing-service
        action: Consumes shipment.delivered, captures pre-authorised payment

      - step: 6
        service: notification-service
        action: Consumes shipment.delivered, sends delivery confirmation to customer

    failure_modes:

      - step: 1
        failure: Invalid carrier signature
        impact: Webhook rejected with 401. Carrier retries according to their schedule.
        investigation: Verify carrier secret rotation in Secrets Manager matches carrier config.

      - step: 5
        failure: Payment capture fails after delivery
        impact: Order marked COMPLETED but payment not captured. Revenue gap.
        investigation: Check billing-service DLQ. Stripe pre-auth expires after 7 days.

domain_rules.yaml

Non-negotiable rules that apply across the platform — data residency, security patterns, API contracts, service ownership. The primary file for code review and architectural decisions.

data_residency:

  customer_pii:
    definition: name, email, phone, address, payment instrument identifiers
    rule: Must remain in primary region at rest. Replication to DR region requires encryption with customer-managed keys.
    services_in_scope: [order-service, billing-service, notification-service, customer-portal]
    logging: PII fields must never appear in application logs. Use masked representations.

  payment_data:
    definition: card numbers, CVV, full PANs
    rule: PCI-DSS scope. Tokenize at point of entry. Never persist raw card data. Never log.
    services_in_scope: [billing-service]
    notes: Tokens from payment gateway are not PCI-sensitive and may be stored.

  shipment_location:
    definition: warehouse coordinates, carrier depot addresses, last-known delivery location
    rule: No residency restriction. May be replicated globally for carrier API latency.
    services_in_scope: [shipment-tracking, carrier-integration, warehouse-management]

security:

  authentication:
    pattern: JWT bearer tokens issued by auth service. Validated at API gateway.
    services: Gateway enforces auth. Internal services trust gateway-forwarded claims.
    service_to_service: mTLS within cluster mesh. No inter-service call bypasses the mesh.

  sensitive_endpoints:
    - pattern: /admin/**
      rule: Requires admin role claim in JWT. All requests audit-logged with actor identity.
    - pattern: /payments/**
      rule: PCI scope. Additional WAF rules apply. Response bodies must not include raw card fields.
    - pattern: /internal/**
      rule: Not routable from public API gateway. Internal mesh only.

contracts:

  api_versioning:
    pattern: URL-based versioning (/v1/, /v2/)
    breaking_change_policy: New version required. Previous version supported for 90 days minimum.
    deprecation: Deprecation header added to responses. Communicated via internal changelog.

  event_schema:
    registry: Avro schemas in schema registry — one schema per topic
    change_policy: Backward-compatible changes only on existing topics (add optional fields, never remove).
    breaking_changes: New topic required. Consumer migration window negotiated per case.

  database_ownership:
    rule: Each service owns exactly one database schema. No service reads another service's schema directly.
    cross_service_data: Always via API or event. No shared tables, no cross-schema joins.

  shared_libraries:
    rule: Platform shared libraries (auth utils, event serialisers) are versioned independently.
    upgrade_policy: Services must adopt new major versions within one PI cycle.

coding_patterns.yaml

Package structure, naming conventions, testing patterns. Consumed when creating new files or services, or reviewing code for structural correctness.

architecture:
  style: Hexagonal architecture (ports and adapters) with domain-driven design
  principle: Business logic lives in the domain layer with zero framework imports.

package_structure:
  layout: |
    src/main/java/com/{org}/{service-name}/
    ├── domain/
    │   ├── model/          # entities, value objects
    │   ├── service/        # domain services
    │   └── port/           # inbound & outbound ports
    ├── application/
    │   └── usecase/        # one class per use case
    ├── infrastructure/
    │   ├── persistence/    # JPA repositories
    │   ├── messaging/      # Kafka/SQS adapters
    │   └── client/         # HTTP clients
    └── api/
        ├── controller/     # REST controllers
        ├── dto/            # request/response objects
        └── mapper/         # DTO ↔ domain mappers

naming:
  services: kebab-case (order-service, shipment-tracking)
  java_packages: com.{org}.{servicename} — no hyphens in package names
  events: noun.past-tense (order.created, shipment.delivered, payment.confirmed)
  database_tables: snake_case, prefixed by domain (order_items, shipment_events, billing_transactions)
  api_resources: plural nouns (/orders, /shipments, /customers)

testing:
  unit:
    scope: Domain layer only
    tools: JUnit 5, Mockito
    rule: No Spring context in unit tests. Pure Java.
  integration:
    scope: One test class per infrastructure adapter
    tools: Testcontainers (PostgreSQL, LocalStack for SQS)
    rule: Test the adapter in isolation — not the full application stack.
  contract:
    scope: All synchronous API calls between services
    tools: Pact (consumer-driven)
    rule: Consumer defines the contract. Provider verifies it in CI.
  end_to_end:
    scope: Happy path per named flow only
    tools: Custom test harness against staging environment
    rule: E2E tests are not a substitute for integration tests.

error_handling:
  pattern: Domain exceptions mapped to HTTP status codes in the API layer only.
  rule: Never throw HTTP exceptions from domain or application layer.
  async_failures: All consumer failures must route to a DLQ. No silent drops.

Layer 3 — Work History

A SQLite database (tracker.db) records every git commit across all repositories in the workspace. A post-commit hook, installed at workspace setup, writes each commit automatically.

The commit hook extracts the task ID from the branch name or commit message using a configurable pattern, associates commits with tasks, and writes changed file paths for granular history.

The database is queryable by the agent during any session:

-- What have I worked on this sprint?
SELECT DISTINCT message, repo FROM commits
WHERE branch LIKE '%sprint-42%'
ORDER BY timestamp DESC;

-- Which repos does TASK-1234 touch?
SELECT DISTINCT repo FROM commits
WHERE message LIKE '%TASK-1234%';

-- What did I actually change in order-service last week?
SELECT message, files, timestamp FROM commits
WHERE repo = 'order-service'
AND timestamp >= DATE('now', '-7 days')
ORDER BY timestamp DESC;

This completes the context layer and can be fetched as and when needed. Different skills can be configured on top of this. It’s just plug and play after this.

The ones I use most and help me with the data fragmentation are:

/platform is the one I actually reach for most, by habit, not as a fallback. It’s the general-purpose query skill: ask it anything about the platform, a Helm value, where a field lives, why something is wired the way it is, and it pulls from whichever context files the question needs, falling into the actual codebase if the context files don’t have the answer.

The next two are about judgment before a change ships.

/impact takes a description of a change, an event, an endpoint, a service, and produces a blast radius report: direct consumers, indirect dependents, infrastructure implications, and any domain rule the change runs into. Ask it about changing the payload of an event, and it finds every consumer of that event in system_map.yaml, checks for external consumers, and flags the schema change policy from domain_rules.yaml, all before any code gets written.

/data-lineage takes a data concept, a field, a customer identifier, and traces where it originates, how it flows between services, and what rule governs it. Ask about a payment instrument and it walks from the entry point through tokenization, confirms raw card data never leaves the one service allowed to touch it, and cites the residency rule that says so.

The next three are about debugging, working from different starting points toward the same flow knowledge.

/flows takes a flow name and narrates it step by step in plain language, what each service does, what can go wrong at each point, and what to check first. Good for understanding a flow before touching it, or briefing someone new.

/trace takes an actual stack trace or error and maps it onto a flow step and a service boundary, then produces a concrete investigation path grounded in the real call chain rather than generic debugging advice.

/why takes a symptom in plain language, “customers aren’t getting confirmation emails,” and works backward to a ranked list of failure hypotheses across the relevant flow. /trace starts from an error. /why starts from a feeling that something’s wrong and has no error to anchor on yet. Or any general question about why something is the way it is, and it will find the answer in the context files or codebase.

The remaining two handle code-level work, accomplishment tracking, and keeping everything honest.

/resume particularly useful when I am resuming work on tasks after a break or working on multiple tasks in a day. Fetches data from tracker db and PRs, JIRA and gives auto loads the context for the agent. I don’t have to manually tell what all has been done yet and what are involved services. Just do /resume task-id.

/context-refresh is the maintenance mechanism, and it runs in three modes: a shallow pass that catches new or removed services from config headers and git log in minutes, a service-scoped pass you run right after merging a change to keep one entry current, and a full deep crawl that rebuilds all four files from scratch at the start of a sprint or after major platform changes. Every run backs up the previous version, so rolling back is a single file rename. Separately, I’m building a small scheduled agent on Atlassian Studio, still in progress, meant to trigger a version of this automatically rather than relying on me to remember to run it.


What This Looks Like in Practice

Two real mornings, both /platform.

  1. I was updating a service to read from an internal S3 bucket instead of an external one. I described the change, nothing about permissions, nothing about ownership. Before suggesting any code, it flagged that the bucket belonged to a different service and, per the access rule in domain_rules.yaml, recommended I raise a ticket for access first. I hadn’t mentioned the rule or the bucket’s owner. It crossed system_map.yaml (who owns the bucket) against domain_rules.yaml (the access rule) on its own, because both files existed and applied to what I was doing.

  2. In another instance, it needed the old bucket name for a migration step, and pulled it straight from the relevant Helm values file using the path already recorded in system_map.yaml, without me saying which repo or which environment overlay to check.

The measure of a good context file is not completeness. It is whether the agent can answer the questions that matter most without ever opening a repository.


I’m looking at converting this into an MCP next, partly for efficiency, partly to see if it travels beyond my own terminal. If you build something similar or have ideas on where this breaks down, I’d like to hear about it.