Blog

Rightsize a Monolith API: Extract What Consumers Actually Need

Kin Lane ·May 14, 2026
Table of contents

I have been staring at another monolith API this week. Two hundred and thirty operations. Seven years of organic growth. Auth scheme that shifted somewhere around operation number ninety. A handful of internal consumers, a handful of external partners, and every one of them is using a tiny slice of the surface area — but nobody has ever actually drawn a line around which slice.

That is the shape of use case number seven in the Naftiko Framework use cases guideRightsize a Monolith API. Extract focused capabilities from a broad monolith so consumers get only what they need for each use case.

This is part seven of the nine-part walk. Earlier posts covered AI integration, rightsize AI context, elevating existing APIs, elevating Google Sheets, composing AI context, and rightsizing microservices. Monolith rightsizing is the mirror image of the microservices one — instead of too many tiny services, we have one very big one.

The shape of the problem

Let me describe what I keep seeing.

A product team wants to build something. A dashboard, a copilot, a partner integration, a mobile client. They get pointed at the monolith API. They open the Swagger page and find two hundred endpoints. Ninety of them are deprecated but still live. Forty are internal-only but not marked as such. The rest are a mix of what they need and what they absolutely must not touch.

So they do what any reasonable engineer does. They pick out the six operations that matter, copy the request shapes into their own code, and build against those. Six months later, another team does the same thing. A year later, the platform team cannot tell you who uses what, because every consumer is reaching into the monolith directly and picking its own six operations out of the pile.

That is the sprawl problem, one direction deeper. It is not the monolith that is broken. The monolith is fine. The surface is wrong for every actual consumer.

What a capability changes

The Naftiko capability spec lets you draw a line around a consumer-shaped slice of the monolith and publish that slice as its own thing — without rewriting the monolith, without forking anything, without moving a single endpoint.

Two things carry the weight of this use case.

First, consumes is selective. You declare only the operations that matter for this consumer scenario. The other two hundred endpoints are not in the spec because they are not in the capability.

Second, exposes is task-shaped. You remap those selected operations to resources and tools that are named for what the consumer is actually trying to do, not for how the monolith happened to organize itself. And for the paths where a full transformation would be overkill, there is forward.targetNamespace — a pass-through that routes the request straight through the capability layer, under the capability’s own namespace.

Here is what it looks like when a “partner order insights” slice gets carved out of a monolith that also handles billing, inventory, support, user management, and twenty other things:

naftiko: "1.0.0-alpha1"

info:
  title: Partner Order Insights
  description: "Focused slice of the commerce monolith  order reads and partner-scoped cancellation"

capability:
  consumes:
    - namespace: commerce-monolith
      type: http
      baseUri: "https://api.commerce.internal"
      authentication:
        type: bearer
        token: "{{COMMERCE_API_TOKEN}}"
      resources:
        - name: orders
          path: "/v1/orders"
          operations:
            - name: get-order
              method: GET
              path: "/v1/orders/{orderId}"
              inputParameters:
                - name: orderId
                  in: path
                  type: string
                  required: true
            - name: list-partner-orders
              method: GET
              path: "/v1/orders"
              inputParameters:
                - name: partnerId
                  in: query
                  type: string
                  required: true
                - name: status
                  in: query
                  type: string
            - name: cancel-order
              method: POST
              path: "/v1/orders/{orderId}/cancel"
              inputParameters:
                - name: orderId
                  in: path
                  type: string
                  required: true

  exposes:
    - type: rest
      basePath: "/api/partner-orders"
      forward:
        targetNamespace: commerce-monolith
        trustedHeaders:
          - X-Partner-Id
      endpoints:
        - method: GET
          path: "/{orderId}"
          description: "Get a single partner order, slimmed"
          call: commerce-monolith.get-order
          outputParameters:
            - type: object
              properties:
                - name: orderId
                  type: string
                  mapping: "$.id"
                - name: partnerId
                  type: string
                  mapping: "$.partner.id"
                - name: status
                  type: string
                  mapping: "$.status"
                - name: total
                  type: number
                  mapping: "$.amounts.total"
        - method: GET
          path: "/"
          description: "List orders for a partner, task-shaped"
          call: commerce-monolith.list-partner-orders

    - type: mcp
      namespace: partner-orders
      tools:
        - name: get-partner-order
          description: "Get a single partner order by ID, fields the copilot actually needs"
          hints:
            readOnly: true
            idempotent: true
          call: commerce-monolith.get-order
          inputParameters:
            - name: orderId
              type: string
              required: true
              mapping: "pathParameters.orderId"
          outputParameters:
            - type: object
              properties:
                - name: orderId
                  type: string
                  mapping: "$.id"
                - name: status
                  type: string
                  mapping: "$.status"
                - name: total
                  type: number
                  mapping: "$.amounts.total"
        - name: cancel-partner-order
          description: "Cancel a partner order"
          call: commerce-monolith.cancel-order
          inputParameters:
            - name: orderId
              type: string
              required: true
              mapping: "pathParameters.orderId"

Three upstream operations. A dozen upstream fields per response. A couple of authentication and header rules.

What a consumer sees is a small REST surface at /api/partner-orders, an MCP surface called partner-orders, and a forward block that quietly routes anything that does not need reshaping straight through to the monolith under the capability’s own namespace — with trusted-header passthrough so the upstream authorization layer still works.

Three dimensions, one spec

Technology. The capability does not rewrite the monolith. It does not cache responses. It does not proxy in the bad sense. It selectively consumes, remaps to narrower exposed resources and tools, and filters output down to the fields a consumer needs. And where filtering would be silly — for instance, when you want a pure pass-through for a known set of routes — forward.targetNamespace does the routing without making you re-declare every operation.

Business. The monolith team does not have to approve a new release. The consumer team gets a stable, narrow contract that lives in Git as a file. Deprecation inside the monolith becomes a capability-layer decision, not a global break. Two consumer teams with different needs get two different capabilities against the same monolith — and neither one has to care what the other is doing.

Politics. This is where I think most “extract from the monolith” conversations quietly die. The usual answer to “the API is too broad” is “we need to break up the monolith.” That is a multi-year project with a platform team on one side and a business owner on the other, and nobody wants to own it. The capability spec sidesteps the whole argument. You do not break up the monolith. You draw consumer-shaped lines around it. The monolith keeps running, and the surface everybody actually consumes is smaller, named, and governed.

Features that matter for this use case

The wiki calls out the specific features that come into play for this use case, and they line up with what I just walked through:

  • Selective operation exposure from a broad API — you declare only the operations you need
  • Remap consumed operations to narrower exposed resources — the exposed shape is task-named, not API-named
  • Output filtering via typed parameters and JSONPath mapping — consumers get the five fields that matter, not the forty the monolith returns
  • Forward proxy for pass-through routesforward.targetNamespace for routes that do not need reshaping
  • Trusted header forwarding — identity and partner context survives the pass-through
  • Dual-channel exposure from one capability — the same slice feeds REST consumers and MCP agents

These are not abstract. Every one of them maps to a pattern I have seen go wrong in bespoke code.

Where I am using this

The partner-orders shape above is a lightly anonymized version of what several of the partner capabilities I have been writing look like in practice. The upstream API is always broader than any one consumer needs. The capability is always the narrower, task-shaped thing the consumer can actually live with.

The interesting conversation is not “should we break up the monolith.” It is “what is the capability-sized slice that this consumer needs, and who owns that slice going forward?” That is a conversation a platform team can actually have. It finishes. It produces a file.

Next

Next in the series is Capability-first context engineering — starting from the MCP contract instead of the API, and letting the consumes block catch up.

Still walking the list.