<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Justin</title>
    <description>The latest articles on DEV Community by Justin (@jusjustin).</description>
    <link>https://dev.to/jusjustin</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3822349%2Fa98f1d73-2613-4dff-9eeb-471ef29d6ed8.png</url>
      <title>DEV Community: Justin</title>
      <link>https://dev.to/jusjustin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jusjustin"/>
    <language>en</language>
    <item>
      <title>Calling Services Over Azure Service Bus Without Writing a Client Layer</title>
      <dc:creator>Justin</dc:creator>
      <pubDate>Mon, 19 Oct 2026 11:38:58 +0000</pubDate>
      <link>https://dev.to/jusjustin/calling-services-over-azure-service-bus-without-writing-a-client-layer-4ehd</link>
      <guid>https://dev.to/jusjustin/calling-services-over-azure-service-bus-without-writing-a-client-layer-4ehd</guid>
      <description>&lt;h2&gt;
  
  
  &lt;strong&gt;TL;DR&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Azure Service Bus guarantees delivery between services, but it never writes the code that turns a delivered message into a typed method call with a result, and that layer is where request and reply calls go wrong under load. A missed session filter or a mismatched correlation field doesn't throw an error, it just looks like a slow caller. This piece covers what a correct Service Bus call requires by hand, where that work gets expensive, and how a transport plugin removes the client layer without changing anything about Service Bus itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Getting Started: What Azure Service Bus Handles and What It Leaves to You&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Azure Service Bus is a managed message broker that sits between services, accepting a message from a producer and delivering it to a consumer without either side needing to be online at the same moment. It supports two core patterns: queues, where each message goes to exactly one consumer, and topics with subscriptions, where each message can go to several independent consumers at once.&lt;/p&gt;

&lt;p&gt;Teams reach for Service Bus for the same reason they reach for any managed broker, reliable delivery, durability, and platform-handled ordering without running that infrastructure themselves. What Service Bus doesn't hand you is the code that turns a delivered message into an actual method call with a typed result. That part stays with whichever team is consuming the message, and it's easy to underestimate until a reply routes to the wrong caller or a failure disappears into a timeout with no clear cause.&lt;/p&gt;

&lt;p&gt;The running example for this piece is a service that runs a physics calculation and returns a result. Several callers across a system need to invoke it, and each one needs its own reply to come back correctly, even when three or four calls are in flight on the same reply queue at once. Getting that right by hand is where most of this piece's complexity lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What Building a Correct Service Bus Call Requires&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Getting a request and reply call working correctly over Service Bus means solving three separate problems, and it's easy to get the first two working in a demo and only discover the third one is broken once two callers hit the service at the same time. A request has to reach the right queue. A reply has to find its way back to the specific client that asked for it, not just whichever client happens to be listening on the reply queue. And a server-side failure has to reach the caller as something recognizable as a failure, not as a message that quietly never shows up.&lt;/p&gt;

&lt;p&gt;That middle problem is where teams go wrong. Without a session strategy, two clients sharing a reply queue can end up reading each other's responses, or one client blocks while waiting for a reply that has already been delivered elsewhere. Solving it by hand looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Everything below exists only to get a reply back to the right caller; none of it is business logic
from azure.servicebus import ServiceBusClient, ServiceBusMessage
import uuid

def call_physics_calculator(payload: bytes) -&amp;gt; bytes:
    session_id = str(uuid.uuid4())
    with ServiceBusClient.from_connection_string(CONN_STR) as client:
        sender = client.get_queue_sender(queue_name="myqueue")
        receiver = client.get_queue_receiver(
            queue_name="myqueue.reply", session_id=session_id
        )
        message = ServiceBusMessage(payload)
        message.reply_to_session_id = session_id
        sender.send_messages(message)

        reply = receiver.receive_messages(max_wait_time=30)
        if not reply:
            raise TimeoutError("no response within timeout")
        return b"".join(reply[0].body)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of this describes what the physics calculation actually does. It's session setup, a reply filter, and a timeout, three things a team has to get right before any real logic runs, and get wrong silently if they don't.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Where the Integration Work Piles Up as Patterns Change&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The shape of this work changes depending on how a service consumes Service Bus, but it doesn't get smaller:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A simple queue consumer&lt;/strong&gt; needs SDK setup and message deserialization written by hand before any downstream call happens, and the consumer's expected schema can drift from what the producer actually sends with no build-time warning.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Request and reply over queues&lt;/strong&gt; needs everything a simple consumer needs, plus session generation, a session-filtered receiver, and correlation matching. A missed session filter or a mismatched correlation field means a reply lands with the wrong caller, or never arrives at all.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Topics and subscriptions&lt;/strong&gt; need SDK setup and deserialization repeated per subscription, so every subscription reimplements the same consumer logic even when the message shape is identical across all of them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Request and reply is the expensive one of the three. Sessions exist so a reply queue can be shared safely across many callers at once, but that safety only holds if the session filter, the reply-to field, and the correlation match are all correct, and a mistake in any one of them doesn't throw an error. It just looks like latency.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Cost of Writing This Layer by Hand&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Backend teams commonly lose 30 to 40 percent of engineering time to integration code in general, HTTP clients, DTO drift, SDK versioning, and Service Bus consumers carry that same tax plus a few costs specific to the broker:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Session plumbing per caller&lt;/strong&gt;: every service that needs request and reply correlation reimplements session generation and session-filtered receivers on its own, even when three other teams are calling the same downstream service.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Silent correlation bugs&lt;/strong&gt;: a session or correlation mismatch doesn't throw an error, it just means a caller times out waiting for a reply that already went somewhere else.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manual timeout and retry logic&lt;/strong&gt;: every hand-written client ends up with its own version of this, rarely consistent with how any other caller of the same service handles it.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema drift above the broker&lt;/strong&gt;: nothing enforces that a consumer's expected message shape still matches what the producer sends, the same problem REST has, just moved to sit above Service Bus instead of behind an endpoint.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this shows up on an architecture diagram. It shows up in how long it takes to debug a caller that's timing out for no visible reason, and in how many times different teams rewrite the same session-handling code for the same dependency.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr9qpvs78gufexmoq9kph.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr9qpvs78gufexmoq9kph.png" alt="Graftcode" width="512" height="341"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;A Transport Plugin That Handles Sessions and Correlation for You&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Graftcode is a runtime-level integration platform, and its Azure Service Bus plugin lets a service call route through Service Bus without writing any session or correlation code by hand. The architecture underneath is the same three pieces Graftcode uses everywhere: a Graft, a strongly typed interface installed via a package manager that mirrors the provider's public methods, GraftConfig, an internal setting of each Graft that decides whether a call runs in process or through a configured transport, and the Graftcode Gateway, which runs and hosts the provider service itself.&lt;/p&gt;

&lt;p&gt;On top of that, the plugin offers two modes, set purely by configuration, request and reply over queues for methods that return something, and one-way over a topic for methods that don't. Configuring the client side of a request and reply call looks like this in C#, the only language this configuration pattern is currently confirmed for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;graft.nuget.PhysicsCalculator.GraftConfig.SetConfig("""
{
  "configurations": {
    "graft.nuget.PhysicsCalculator": {
      "runtime": "netcore",
      "stateless": true,
      "plugin": {
        "name": "ServiceBusPlugin",
        "connectionString": "Endpoint=sb://&amp;lt;namespace&amp;gt;.servicebus.windows.net/;SharedAccessKeyName=&amp;lt;keyName&amp;gt;;SharedAccessKey=&amp;lt;key&amp;gt;",
        "queue": "myqueue",
        "replyQueue": "myqueue.reply",
        "rpcTimeoutMs": 30000
      }
    }
  }
}
""");
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything the earlier hand-written snippet did now happens inside the plugin. Session generation reuses the request's own correlation ID, and the reply receiver attaches a session filter scoped to that session, the same filter mechanism the official .NET and Java SDKs use. Matching is a little more forgiving than a single rigid field too, either the correlation ID or the message ID is accepted. A server-side failure reaches the caller the same way a local exception would, the response's first byte doubles as a sentinel, where a specific value means everything after it is an error message, thrown client-side as a native exception, and any other value means a normal response.&lt;/p&gt;

&lt;p&gt;The one-way mode skips sessions and correlation entirely, since there's no reply expected. A client publishes to a topic and returns immediately, and the server consumes from a topic subscription without sending anything back. Multiple subscriptions on the same topic each receive their own independent copy of the message, this is native Service Bus pub and sub, not something built on top of the plugin.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;How This Extends Your Existing Service Bus Setup&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Adopting this plugin doesn't ask a team to change how it already uses Service Bus. Whatever namespace, session, and dead-lettering configuration already exists stays exactly as it is, and the plugin gets adopted per service pair, usually starting with whichever one currently has the most hand-written session and correlation code sitting on top of it.&lt;/p&gt;

&lt;p&gt;It also doesn't touch anything Service Bus already does well, durability, ordering, and dead-lettering are unaffected. What it removes is the layer above that, client setup, session handling, correlation matching, and the deserialization a consumer would otherwise write by hand.&lt;/p&gt;

&lt;p&gt;Local development splits into two genuinely separate options worth keeping apart:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pointing at the official Service Bus emulator&lt;/strong&gt;, which still runs something locally but removes the dependency on a live Azure namespace during testing.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Setting GraftConfig to in-process mode&lt;/strong&gt;, which skips the broker entirely so the call never leaves the process, useful for fast logic iteration without needing an emulator or a namespace at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's what changes in practice between writing this layer by hand and letting a plugin handle it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Session handling&lt;/strong&gt;: manual session generation and a session-filtered receiver written per caller, versus session handling that lives inside the plugin.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correlation&lt;/strong&gt;: manual matching between request and reply, versus automatic matching with a documented fallback field.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failure visibility&lt;/strong&gt;: a dropped or mismatched reply that just looks like a timeout, versus an error that crosses the wire as a defined signal and throws as a native exception.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple subscriptions&lt;/strong&gt;: separate consumer code written per subscription even for identical message shapes, versus one configured plugin path regardless of subscription count.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local testing&lt;/strong&gt;: a real namespace or a manually configured emulator client, versus GraftConfig's in-process mode or the plugin's own emulator support.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn't a claim that the plugin removes coupling between services, that depends on how boundaries are drawn, not on what sits underneath a call. What it removes is the session, correlation, and client code that adds overhead on top of a service boundary that's already reasonably drawn.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Where Should You Start&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Service Bus solves delivery well, durability, ordering, and dead-lettering are the broker's job, not the consuming service's. What it never solved was the code a team writes to turn a delivered message into a typed method call, and that code carries real session and correlation risk regardless of how reliable the underlying broker is. The pattern that keeps causing production incidents isn't a Service Bus limitation, it's a missing session filter or a mismatched correlation field that nobody notices until two callers collide on the same reply queue.&lt;/p&gt;

&lt;p&gt;If your team is currently maintaining hand-written session and correlation code for a request and reply call over Service Bus, that's the specific place to start looking at whether a transport plugin would help, not a wholesale migration. &lt;a href="https://www.graftcode.com/" rel="noopener noreferrer"&gt;Explore Graftcode&lt;/a&gt; to see how the Service Bus plugin fits your setup, or go to the &lt;a href="https://academy.graftcode.com/" rel="noopener noreferrer"&gt;Graftcode Academy&lt;/a&gt; for the full configuration reference and the other broker plugins currently available.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;FAQs&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Does the plugin change any of Service Bus's own delivery guarantees?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;No. Durability, ordering, dead-lettering, and session-based delivery are all Service Bus features and stay exactly as configured. The plugin only removes the hand-written client, session, and correlation code that would otherwise sit above those guarantees.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;How does a server-side exception actually reach the calling code?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The response payload carries a one-byte sentinel. A specific first-byte value means everything after it is read as an error message and thrown client-side as a native runtime exception. Any other first byte means a normal successful response, so error handling on the calling side looks the same as it would for a local method call.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Can a service use both request and reply and one-way calls at the same time?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Yes. The mode is chosen per configuration, not system-wide. One service call can be configured for request and reply over queues while another is configured for one-way over a topic, depending on whether each method returns a value.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Is there a message size limit to plan around?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Yes. Azure Service Bus caps standard-tier messages at 256 KB and premium-tier messages considerably higher, and the plugin enforces its own ceiling around 1 MB in its current source. A method returning a large object can hit this in a way it wouldn't over a direct connection, so a large payload either needs a higher tier or should stay on a direct-connection transport instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;What's the practical difference between a Service Bus queue and a topic?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;A queue delivers each message to exactly one consumer, which is what request-and-reply calls use. A topic delivers each message to every subscription attached to it, so multiple independent consumers can each get their own copy of the same message. That's why one-way, fire-and-forget calls use a topic and request and reply calls use a queue.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Does adopting a transport plugin mean migrating everything onto Service Bus?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;No. It's built to extend an existing setup incrementally. A team can configure it for a single service pair where session and correlation overhead is highest, while every other service pair keeps running however it already does. You don't need to migrate an entire system to adopt it for one call.&lt;/p&gt;

</description>
      <category>azure</category>
      <category>devchallenge</category>
      <category>devops</category>
      <category>finops</category>
    </item>
    <item>
      <title>Backend for Frontend Pattern: When and Why to Use It</title>
      <dc:creator>Justin</dc:creator>
      <pubDate>Thu, 15 Oct 2026 10:45:48 +0000</pubDate>
      <link>https://dev.to/jusjustin/backend-for-frontend-pattern-when-and-why-to-use-it-4m8m</link>
      <guid>https://dev.to/jusjustin/backend-for-frontend-pattern-when-and-why-to-use-it-4m8m</guid>
      <description>&lt;h2&gt;
  
  
  &lt;strong&gt;TL;DR&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A Backend for Frontend gives each client type, web, iOS, Android, its own dedicated backend shaped around what that interface actually needs to render, instead of forcing every client to filter down one shared, general-purpose API response. The pattern solves a real client-shaping problem, but it comes with a cost: duplicated integration logic across services, more deployments to operate, and an added network hop in every request path. This piece covers what a BFF actually does, how it differs from an API gateway, what running several of them costs, and where the integration layer inside each one can be trimmed down.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Getting Started: What a Backend for Frontend Pattern Solves&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A Backend for Frontend, usually shortened to BFF, is a server-side service built and owned by the team responsible for one specific client, web, iOS, Android, or a third-party integration, and positioned between that client and the backend services it depends on. Instead of one general-purpose API shaped to satisfy every consumer at once, each frontend gets a backend shaped around what that interface actually needs: the fields it renders, the requests it can afford to make, and the aggregation it would otherwise have to do on-device.&lt;/p&gt;

&lt;p&gt;The pattern traces back to engineering teams at REA and SoundCloud, who ran into the same problem from different directions: a shared backend serving a growing set of very different clients kept accumulating logic for all of them at once. Phil Calçado's original write-up on the pattern, later catalogued by Sam Newman as a named architectural pattern, put the fix plainly: build one backend per user experience rather than one general-purpose backend for everyone. A mobile client and a desktop client don't just render the same data differently, they operate under different constraints entirely, screen space, battery, and network reliability among them, and a shared backend has no clean way to represent that difference in a single schema.&lt;/p&gt;

&lt;p&gt;Ownership is the part of the definition worth noting early. A BFF isn't an API split by client type at the routing level. It's typically built and deployed by the same team that owns the frontend it serves, using whatever language and release cadence fits that team, independent of how the core backend services are versioned or shipped.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo2b8xgt8iyne8bk2gv45.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo2b8xgt8iyne8bk2gv45.png" alt="Graftcode" width="512" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every product that ships more than one client eventually runs into the same argument: should all of them talk to the same backend, or should each one get something closer to what it actually needs. For years the default answer was one shared backend, reused across every client because building a new API per interface felt like duplicated infrastructure. That default holds up fine with one client and a handful of endpoints. It stops holding up once a mobile app on a metered connection and a desktop dashboard with a dozen open panels are both hitting the same general-purpose response, and someone has to decide whose needs the next schema change should serve.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Why a Single Shared Backend Strains Across Multiple Clients&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A shared backend degrades in a specific, predictable way as the number of client types grows. It doesn't fail outright, it accumulates conditional logic, optional fields nobody wants to remove because some client still depends on them, and query parameters that exist only to let one interface ask for less than everyone else gets by default.&lt;/p&gt;

&lt;p&gt;The practical symptoms show up as three separate but related pressures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Growing functionality.&lt;/strong&gt; The backend's feature surface keeps expanding to cover every platform it serves, which makes it harder to reason about and slower to change safely.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Team bottlenecks.&lt;/strong&gt; Frontend teams have to coordinate with a separate backend team before shipping anything that needs a new field or a different shape, turning a frontend release into a cross-team negotiation.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forced aggregation.&lt;/strong&gt; Combining data from several downstream services into one response becomes the shared backend's job for every client at once, rather than something each client can shape around its own needs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these problems are really about scale in the infrastructure sense, they're about ownership and shape. A backend that has to satisfy a phone app and a desktop dashboard at the same time is optimizing for neither, and the team that owns it ends up mediating a conflict of interest between two frontends that don't need to agree on anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;How a Backend for Frontend Differs From an API Gateway&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The BFF pattern gets confused with an API gateway often enough that it's worth stating the distinction directly, since the two solve different problems and most systems that use one still need the other.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Client awareness.&lt;/strong&gt; An API gateway is client-agnostic, applying the same routing and policy layer to every caller. A BFF is client-aware, built specifically for one frontend's needs.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Typical owner.&lt;/strong&gt; A platform or infrastructure team usually owns the gateway. The frontend team the BFF serves owns the BFF.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Core job.&lt;/strong&gt; A gateway handles routing, authentication, rate limiting, and other cross-cutting concerns across services. A BFF handles aggregating and reshaping data into what one specific UI needs.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What triggers a change.&lt;/strong&gt; A gateway changes on platform-wide policy updates. A BFF changes on that frontend's own release cycle.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The structural difference comes down to what each layer is centered on. An API gateway is infrastructure-centric, applying the same routing and security policy to every request regardless of who's calling. A BFF is application-centric, built around one frontend's specific needs and evolving on that frontend's own schedule. A system commonly runs both together: an API gateway handling routing, authentication, and rate limiting for every service, with one or more BFFs sitting behind it, each shaping a response for its own client.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Where a BFF Sits Inside a Microservices Call Path&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;In a typical microservices system, a single screen rarely maps to a single downstream service. A dashboard home screen might need workspace metadata from one service, usage figures from a second, and recent alerts from a third. Without a BFF, the client itself has to make three separate calls and stitch the results together on-device.&lt;/p&gt;

&lt;p&gt;A BFF moves that aggregation off the client and onto a service the frontend team controls. The client makes one call to its BFF, the BFF fans out to each downstream service it needs, combines the results into the shape that screen actually renders, and returns a single response. The downstream services stay unaware a BFF exists between them and the client, they're just handling requests from another internal caller.&lt;/p&gt;

&lt;p&gt;This is also where the pattern's real engineering cost lives. Every BFF that aggregates from multiple services needs a client for each of those services, something to deserialize each response, and error handling for when one of those calls fails while the others succeed. That code has nothing to do with what the BFF is actually for, shaping a response for one frontend, and it's the same category of integration work a shared backend already had to do, just now duplicated across however many BFFs a system ends up running.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Real Operational Cost of Running Several BFFs&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Adopting the pattern is rarely a question of whether it solves the client-shaping problem, it does. The harder question is whether a team can absorb the ongoing cost of running several BFFs. The most commonly cited costs fall into a few consistent categories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code duplication.&lt;/strong&gt; If a web BFF and a mobile BFF both aggregate from the same three downstream services, the integration logic, the HTTP clients, the response parsing, the retry handling, tends to get written twice, once per BFF, with no natural place for it to live once.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operational overhead.&lt;/strong&gt; Each BFF is its own deployable, with its own release pipeline, its own secrets, and its own service-level objectives to track, so a system with three BFFs has three times the operational surface area of a single shared backend.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Added latency.&lt;/strong&gt; A BFF sits between the client and the services it aggregates from, so every request pays for one additional network hop a direct client-to-service call wouldn't have, which matters more for latency-sensitive interfaces than for ones with looser response-time budgets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's a structural dimension to this too. A BFF's boundaries tend to track team boundaries rather than pure technical ones, since the pattern exists specifically to give a frontend team a backend it can change without waiting on anyone else. That's a genuine advantage while team structure is stable, but a reorganization that merges or splits frontend teams usually forces a corresponding restructuring of the BFFs those teams own, a maintenance cost that has nothing to do with the underlying services at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Deciding Whether the BFF Pattern Is Worth Adopting&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The pattern earns its cost when a shared backend is genuinely straining to serve conflicting client needs, when different frontends would benefit from different languages or release cadences, or when a frontend team is regularly blocked waiting on a separate backend team to ship a change only that frontend needs.&lt;/p&gt;

&lt;p&gt;It's a weaker fit when every client makes roughly the same requests and expects roughly the same shape back, since a BFF built for that case adds an extra hop and a second service without shaping anything meaningfully differently. GraphQL with frontend-specific resolvers already lets each client ask for exactly the fields it needs from a single schema, addressing much of the same over-fetching problem without a separate service per client. And if only one interface actually talks to the backend today, there's no shaping conflict yet to resolve.&lt;/p&gt;

&lt;p&gt;The practical test is whether the client-shaping problem is real and recurring, not hypothetical. A team debating the pattern for a single web client with no mobile app on the roadmap is usually better served by fixing the shared backend's worst over-fetching cases directly than by introducing a second service to maintain.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;How Graftcode Reduces the Integration Code Inside Each BFF&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;None of the BFF pattern's actual job, deciding what a client needs and shaping a response around it, changes with Graftcode in the picture. What changes is the code each BFF writes to reach the services it aggregates from, the hand-written integration layer, HTTP clients, DTO mapping, response parsing, that shows up duplicated across every BFF a team runs, sitting alongside whatever REST, gRPC, or broker setup already connects those services.&lt;/p&gt;

&lt;p&gt;A BFF calling three downstream services today typically means three hand-rolled clients, three sets of response types to keep in sync with whatever each service actually returns, and error handling written separately for each one. Graftcode reduces that layer with a Graft per service: a strongly-typed interface generated directly from that service's public methods and installed through a package manager, the same way a dependency gets installed today. The BFF calls a method directly instead of constructing a request, and gets a strongly-typed object back with no manual deserialization required.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Install a Graft for each downstream service (per-service pip index, per docs.graftcode.com)
python -m pip install --extra-index-url https://grft.dev/simple/&amp;lt;project-id&amp;gt;__free graft-nuget-workspaceservice==1.0.0


# The mobile BFF's dashboard endpoint, aggregating three services through Grafts
from graft_nuget_workspaceservice.graft.nuget.WorkspaceService import GraftConfig as WorkspaceConfig
from graft_nuget_workspaceservice.workspaceservice import WorkspaceService

from graft_nuget_usageservice.graft.nuget.UsageService import GraftConfig as UsageConfig
from graft_nuget_usageservice.usageservice import UsageService

from graft_nuget_notificationservice.graft.nuget.NotificationService import GraftConfig as NotificationConfig
from graft_nuget_notificationservice.notificationservice import NotificationService

WorkspaceConfig.host = "wss://workspace-service-host/ws"
UsageConfig.host = "wss://usage-service-host/ws"
NotificationConfig.host = "wss://notification-service-host/ws"

@app.get("/mobile/dashboard/{workspace_id}")
async def mobile_dashboard(workspace_id: str):
    workspace = WorkspaceService.get_summary(workspace_id=workspace_id)
    usage = UsageService.get_current_period(workspace_id=workspace_id)
    alerts = NotificationService.get_unread(workspace_id=workspace_id)
    return {
        "workspace": workspace,
        "usage": usage,
        "unread_alerts": alerts,
    }

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every object returned above, workspace, usage, alerts, arrives as the same strongly-typed object the provider service defined, with no manual JSON parsing or DTO mapping on the BFF's side. Locally, each GraftConfig can point in-memory instead of at a remote host, so a developer can iterate on the aggregation logic without three downstream services actually running. In staging or production, switching each Graft to its remote host is a configuration change, not a rewrite.&lt;/p&gt;

&lt;p&gt;This doesn't remove the extra network hop a BFF adds, and it doesn't touch what makes the pattern work in the first place, one team owning a service shaped around its own frontend. What it reduces is the integration code sitting inside that service that has nothing to do with shaping a response, and it cuts into the exact duplication problem that shows up whenever more than one BFF calls the same downstream service.&lt;/p&gt;

&lt;p&gt;The duplication problem doesn't fully disappear just because each BFF's calls to a shared downstream service get cleaner. If a web BFF and a mobile BFF both call WorkspaceService, they're still two separate consumers of the same interface, each free to call different methods or handle a response differently. What changes is where that duplication lives. Instead of two hand-written HTTP clients drifting apart as each BFF's team makes its own small adjustments, both BFFs install the same Graft and call the same typed methods, so an interface change on WorkspaceService's side surfaces as a package update to both, rather than a schema mismatch one team discovers in production.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frgfbny8ictg98bme834s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frgfbny8ictg98bme834s.png" alt="graftcode" width="512" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Where Should You Start&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The Backend for Frontend pattern solves a specific, well-understood problem: a shared backend can't shape itself around conflicting client needs without becoming unwieldy for everyone it serves. Splitting that backend by client gives each frontend team a service it can shape and ship on its own terms, at the cost of duplicated integration logic, more services to operate, and an added network hop that has to fit inside each client's latency budget. Whether that trade is worth making depends on how real the shaping conflict actually is today, not on how many client types a roadmap might eventually include.&lt;/p&gt;

&lt;p&gt;For a team that's already decided the pattern fits, the next question is usually how much of each BFF's code ends up being aggregation logic instead of business logic, and whether that ratio is sustainable across however many BFFs the system runs. Explore how a Graft-based approach fits an existing BFF at &lt;a href="https://www.graftcode.com/" rel="noopener noreferrer"&gt;graftcode.com&lt;/a&gt;, or go to the &lt;a href="https://academy.graftcode.com/" rel="noopener noreferrer"&gt;Graftcode Academy&lt;/a&gt; for the full setup and configuration reference.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Frequently Asked Questions&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Does the BFF pattern replace the need for an API gateway?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;No. An API gateway handles cross-cutting concerns, routing, authentication, rate limiting, uniformly across every client and service. A BFF handles client-specific shaping for one frontend. Most systems that use the BFF pattern still run an API gateway in front of it for the concerns a BFF isn't designed to solve.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;How does the BFF pattern compare to using GraphQL with per-client resolvers?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Both address over-fetching and under-fetching, but differently. GraphQL lets each client query a single schema for exactly the fields it needs, while a BFF gives each client a dedicated service instead. A team already running GraphQL with resolvers tuned per client may not need a separate BFF layer on top of it.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;When should a team avoid adopting the BFF pattern?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;When every client makes similar requests and expects a similar response shape, when only one interface currently consumes the backend, or when an API gateway plus the existing shared backend already handles the system's needs without conflict. Adding a BFF in these cases adds operational cost without solving a shaping problem that actually exists.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Does Graftcode replace the Backend for Frontend pattern?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;No. Graftcode doesn't decide how a client's response should be shaped, that's the BFF's job, owned by the frontend team. Graftcode reduces the hand-written integration code a BFF writes to reach the downstream services it aggregates from, without changing what the BFF exposes to its own client.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Does using Graftcode inside a BFF require migrating the services it calls?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;No. A downstream service adopts Graftcode by running a Graftcode Gateway that hosts it and exposes its public methods, nothing about how that service is written has to change. A BFF can then install a Graft for that service one dependency at a time, without requiring every downstream service to adopt it at once.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;How much duplicated code should a team expect across several BFFs before it becomes a real problem?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;There's no fixed threshold, it tracks how many downstream services multiple BFFs share and how often those services' interfaces change. A useful signal is whether engineers are regularly fixing the same integration bug in more than one BFF, that's usually the point where centralizing or typing that layer starts paying for itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Is the BFF pattern only useful for mobile versus web, or does it apply to other client types too?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;It applies to any consumer with meaningfully different needs from the rest, which includes third-party API integrations, internal admin tools, and partner-facing clients, not just mobile versus web. The deciding factor is whether that client's shape or request pattern is different enough to justify its own service, not what kind of client it is.&lt;/p&gt;

</description>
      <category>developer</category>
      <category>webdev</category>
      <category>ai</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Legacy Modernization Without a Full Rewrite: A Practical Guide</title>
      <dc:creator>Justin</dc:creator>
      <pubDate>Tue, 29 Sep 2026 13:51:01 +0000</pubDate>
      <link>https://dev.to/jusjustin/legacy-modernization-without-a-full-rewrite-a-practical-guide-n4l</link>
      <guid>https://dev.to/jusjustin/legacy-modernization-without-a-full-rewrite-a-practical-guide-n4l</guid>
      <description>&lt;h2&gt;
  
  
  &lt;strong&gt;TL;DR&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Legacy modernization is the process of updating systems that have become hard to change or scale, without losing the business logic built into them over years. Most modernization projects don't stall because teams pick the wrong pattern. They stall because teams underestimate the integration cost, the work of bridging what's been extracted back to what remains. This guide covers the Strangler Fig pattern, how to pick a first extraction candidate that won't stall, three supporting patterns for different extraction problems, and where the real cost of modernization actually hides.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Getting Started: What Legacy Modernization Actually Means&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Legacy modernization is the process of updating or replacing software systems that have become difficult to change, scale, or maintain, without losing the business logic and institutional knowledge built into them. Most legacy systems weren't built badly. They were built for the constraints of their time, a smaller team, a different scale, a deployment model that made sense a decade ago. The problem isn't the original decisions. It's what compounds on top of them: new features bolted on, abstractions that leak, a codebase that gets harder to reason about with every change.&lt;/p&gt;

&lt;p&gt;Modernization covers a spectrum of approaches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Replatforming&lt;/strong&gt;: moving the system to new infrastructure without changing the code. Lowest risk, lowest reward.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refactoring&lt;/strong&gt;: restructuring internal code without changing external behavior. Improves maintainability but doesn't touch architectural constraints.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-architecting&lt;/strong&gt;: changing the system's structure, typically monolith to services, while preserving business logic. Higher risk, higher reward.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replacing&lt;/strong&gt;: rewriting from scratch. Justified only when the existing system is fundamentally unsalvageable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This guide covers the space between re-architecting and replacing, moving toward a more flexible architecture while keeping the system live and delivering value throughout.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Why Modernization Projects Actually Stall&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Legacy systems are hard to modernize not because the code is bad, but because the coupling is invisible until you move something. A module that looks self-contained can have tendrils reaching into shared databases, shared utility libraries, and runtime contracts nobody documented.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk1lfwmcufedpsep6cnzm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk1lfwmcufedpsep6cnzm.png" alt="Devto" width="512" height="244"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three failure patterns repeat across modernization projects regardless of team size or stack. The first is the big-bang rewrite: a team estimates 12 months, delivers in 24, and ships a system that no longer matches what users need because requirements shifted mid-rewrite. A Rails monolith being rewritten in Go is a common shape for this. By the time the Go version ships, the original Rails app has absorbed 40 new feature commits the rewrite never accounted for.&lt;/p&gt;

&lt;p&gt;The second is the partial migration that freezes. A team extracts auth and the product catalog cleanly, then hits order management, a module that writes directly to 11 tables shared with inventory, billing, and fulfillment. No clean extraction path exists without touching all four services at once. The migration stalls, and the organization ends up running two systems indefinitely, carrying the complexity of both and the benefit of neither.&lt;/p&gt;

&lt;p&gt;The third is integration cost exceeding extraction value, and it's the most common and least discussed of the three. A Python data service extracted from a Java monolith is clean on its own. Bridging it back to the monolith requires a Java HTTP client, a DTO mapping layer, a versioning strategy, and error handling the original in-process call never needed. The extracted service takes two weeks to build. The bridge takes three. Multiply that across five more extractions in three languages, and the integration work consumes every hour the extraction was supposed to save.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Strangler Fig Pattern Explained in Plain Terms&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The Strangler Fig pattern is the foundational approach to incremental modernization. Named after a vine that grows around a host tree and gradually replaces it, the pattern extracts functionality from a legacy system piece by piece while keeping the system live throughout.&lt;/p&gt;

&lt;p&gt;The core mechanism is a routing layer, an API gateway, a reverse proxy, or an application facade, placed in front of the legacy system. As new services get built, the routing layer progressively shifts traffic from legacy code to the new services. To users and downstream systems, it looks like one system the whole time. Internally, it's a controlled handover in three phases: deploy a routing layer that initially passes everything through unchanged; extract bounded contexts one at a time as independent services, shifting traffic for each; then retire the legacy system once it handles less and becomes a thin shell.&lt;/p&gt;

&lt;p&gt;One distinction worth being precise about: this routing layer is not the same thing as a Graftcode Gateway, which comes up later in this piece. The Strangler Fig routing layer sits in front of the legacy system and directs traffic to it. A Graftcode Gateway isn't a proxy or a traffic router. It's the runtime that hosts a service and exposes its methods to callers. The two solve different problems at different points in the architecture, and conflating them is a common source of confusion when teams start combining the pattern with typed interface tooling.&lt;/p&gt;

&lt;p&gt;The mechanic itself is well understood by now. What separates teams that keep momentum from teams that stall is the decisions around it, which module to extract first, and which supporting pattern to reach for when Strangler Fig alone isn't enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Picking a First Extraction Candidate That Will Not Stall&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The first extraction sets the tone for every one that follows. A hard first candidate teaches the team that extraction is expensive and slow. A good one builds the muscle memory and tooling that make the next four extractions faster.&lt;/p&gt;

&lt;p&gt;A strong first candidate has all five of these properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No shared database tables.&lt;/strong&gt; If a module reads and writes tables other modules also touch, extraction requires resolving data ownership first, which is a separate project on its own.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Synchronous-only callers.&lt;/strong&gt; Modules called only synchronously with a direct response are simpler to route behind a facade. Async producers or consumers add broker configuration to the scope.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stateless or externally managed state.&lt;/strong&gt; A module holding no in-process state, or storing state in Redis or a dedicated database, can scale independently without session affinity concerns.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A narrow interface.&lt;/strong&gt; A module with three or four public methods can be extracted in days. One with 40 endpoints called from across the codebase needs interface auditing first.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Existing test coverage.&lt;/strong&gt; Without tests, running legacy and extracted versions in parallel to compare outputs has no baseline to validate against.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Strangler Fig handles the extraction mechanism. It doesn't automatically solve the integration problem, the work of making the extracted service and the legacy system talk to each other cleanly. That's where the choice of supporting pattern comes in.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Three Supporting Patterns Solve Three Different Problems&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Strangler Fig rarely works alone in a complex migration. Three supporting patterns each solve a distinct problem that surfaces during extraction. The decision isn't which one is best. It's which problem you actually have.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F044tvo93kk2d8e1yjf0q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F044tvo93kk2d8e1yjf0q.png" alt="Image2 showing tables" width="512" height="261"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Branch by Abstraction&lt;/strong&gt; fits when a module has many internal callers. It introduces an interface layer inside the monolith before extraction begins, so callers talk to an interface rather than a concrete implementation. You can then swap the concrete implementation from legacy code to the extracted service without touching any call sites. This matters most when a module is called from dozens of places within the monolith, where updating every call site individually would be disruptive on its own, independent of the extraction itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Parallel Run&lt;/strong&gt; fits when you need to validate behavioral correctness under real traffic before anyone trusts the new path. Requests get sent to both the legacy system and the new service simultaneously, and outputs get compared before committing. Discrepancies are logged but never affect users, since the legacy response is always what gets returned until the new service proves itself. This pattern earns its cost when a silent regression is expensive: payment processing, order fulfillment, anywhere correctness outweighs speed of rollout. Its usual downside is the extra client needed purely for comparison, a second integration surface that exists only to validate, not to serve production traffic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Anti-Corruption Layer&lt;/strong&gt; fits when the legacy domain model shouldn't leak into the new service. It's a translation layer sitting between the legacy system and the new service, converting legacy domain concepts into the new service's domain model. Without it, the new service inherits the legacy system's field names, data shapes, and quirks, and quietly becomes a second legacy system in a new codebase. This matters when the legacy and target domain models genuinely differ: different field semantics, different status representations, different ways of modeling the same concept. When deployed as its own translation service, it typically becomes a cross-language boundary, which is often what makes teams skip building it under timeline pressure.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Common Mistakes That Quietly Compound Integration Cost&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Teams that understand the patterns still hit these repeatedly.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Shared database between monolith and extracted service.&lt;/strong&gt; Teams move the code first and defer data migration to reduce risk, but "later" rarely arrives, and the shared table becomes permanent coupling. A network hop gets added without achieving actual isolation. Use dual-write or change data capture during the transition, then cut over once the new service's data is validated.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migrating too many modules at once.&lt;/strong&gt; Org pressure to show progress across teams leads to parallel extraction, and the integration surface multiplies faster than any one team can track. Every active extraction is a partially bridged system that needs maintenance. Extract sequentially instead, completing one before starting the next.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treating migration bridges as permanent temporary fixes.&lt;/strong&gt; The team that builds the bridge knows it's meant to be replaced. Six months later, a different team owns it, the "temporary" label is long gone from the commit history, and it's carrying production traffic with no test coverage.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Where Graftcode Changes the Cost of Each Pattern&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The three failure patterns above all trace back to the same root cause: the integration layer between what's extracted and what remains. That's where Graftcode changes the calculation: not by changing which pattern is right for a given situation, but by lowering the cost of executing whichever one you pick.&lt;/p&gt;

&lt;p&gt;Consider a Python ML inference service being extracted from a Java monolith, the same cross-language scenario that makes the third failure pattern above so common. Without a typed communication layer, the Java side needs a hand-written HTTP client, DTO definitions matching the Python service's schema exactly, and error handling the original in-process call never needed. If the Python team renames a field, that mismatch doesn't fail at compile time. It shows up as a validation error or a silent divergence under real production traffic, with no signal until it happens.&lt;/p&gt;

&lt;p&gt;With Graftcode, the calling code looks different:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;package orders;

import com.graft.maven.inferenceservice.GraftConfig;
import com.graft.maven.inferenceservice.InferenceService;

public class InferenceCaller {
    static {
        GraftConfig.setConfig(System.getenv("GRAFT_CONFIG"));
    }

    public InferenceResult runInference(String modelId, float[] inputVector) throws Exception {
        // Strongly typed: if Python renames a field, this fails at compile time
        return InferenceService.infer(modelId, inputVector);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;GraftConfig&lt;/code&gt; reads a single &lt;code&gt;GRAFT_CONFIG&lt;/code&gt; environment variable at startup, a structured string specifying the Graft's name, target host, runtime, and module path. Local development points to &lt;code&gt;host=inMemory&lt;/code&gt; and runs the call in-process with no network hop. Production points to the deployed service's Gateway. The calling code in &lt;code&gt;InferenceCaller&lt;/code&gt; never changes between these two states; only the &lt;code&gt;GRAFT_CONFIG&lt;/code&gt; value does.&lt;/p&gt;

&lt;p&gt;That same mechanism applies across the supporting patterns covered earlier. In Branch by Abstraction, the interface stays exactly what it was; what changes is what sits behind it: a Graft instead of a hand-written client, with the monolith-to-service cutover controlled by &lt;code&gt;GraftConfig&lt;/code&gt; rather than a code change. In Parallel Run, the shadow client used purely for comparison becomes a Graft too, so a response shape change during validation surfaces as a reviewable package update instead of a silent mismatch buried in logs. In Anti-Corruption Layer, when the translation layer is deployed as its own service, both integration hops, monolith to ACL, and ACL to new service, become Grafts instead of hand-rolled HTTP clients, removing the doubled integration cost that usually makes teams skip building a standalone ACL under deadline pressure.&lt;/p&gt;

&lt;p&gt;None of this changes which pattern fits a given extraction. It changes whether the pattern that fits is actually affordable to execute.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;When the Strangler Fig Pattern Is Not the Right Choice&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Incremental extraction isn't universally applicable, and it fails or becomes counterproductive in a few specific situations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tightly coupled UIs.&lt;/strong&gt; Server-side-rendered monoliths where the frontend and backend are inseparable are significantly harder to extract with this approach. A micro-frontend decomposition layer is usually the better first move.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch-first systems.&lt;/strong&gt; Systems built around nightly batch processing rather than request-and-response flows don't benefit from facade-based routing, since the pattern assumes real-time traffic that can be progressively redirected.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Broken domain models.&lt;/strong&gt; If the legacy domain model is conceptually wrong rather than just poorly implemented, incremental extraction perpetuates the bad model. Design the target model first, before any extraction begins.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small, isolated systems.&lt;/strong&gt; When a careful rewrite takes weeks, introducing a facade and incremental routing adds overhead the risk reduction doesn't justify.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Where to Start With Your Own Modernization Effort&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Legacy modernization rarely fails because a team picked the wrong pattern. Strangler Fig, Branch by Abstraction, Parallel Run, and Anti-Corruption Layer are all well understood and genuinely effective on their own terms. What derails modernization projects is the integration work between what's been extracted and what remains, work that compounds quietly until it consumes more time than the extraction itself was meant to save.&lt;/p&gt;

&lt;p&gt;Start by identifying a first extraction candidate against the five properties covered above: no shared tables, synchronous callers, externally managed state, a narrow interface, and existing test coverage. From there, match the extraction's actual problem to the right supporting pattern rather than defaulting to Strangler Fig alone. To see how Graftcode fits into whichever pattern you choose, explore &lt;a href="https://www.graftcode.com/" rel="noopener noreferrer"&gt;Graftcode&lt;/a&gt; or go directly to the &lt;a href="https://academy.graftcode.com/" rel="noopener noreferrer"&gt;Graftcode Academy&lt;/a&gt; to get started.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;FAQs&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. What is the difference between the Strangler Fig pattern and a big-bang rewrite?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A big-bang rewrite replaces the entire legacy system in one effort, with no incremental value delivery and risk typically measured in years. The Strangler Fig pattern extracts functionality piece by piece while the legacy system stays live, with each extraction independently deployable and reversible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. How do you choose between Branch by Abstraction, Parallel Run, and Anti-Corruption Layer for a given extraction?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The choice depends on the specific problem the extraction has. Branch by Abstraction fits when a module has many internal callers. Parallel Run fits when correctness under real traffic is critical. Anti-Corruption Layer fits when the legacy domain model is different enough from the target model that it shouldn't be allowed to leak in. These aren't mutually exclusive within a single extraction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. What makes a module a good candidate for the first extraction in a migration?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No shared database tables with other modules, synchronous-only callers, stateless or externally managed state, a narrow interface, and existing test coverage. A candidate missing several of these properties is likely to stall and set the wrong expectations for the extractions that follow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Why do modernization projects usually stall even when the team understands the available patterns?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
 The pattern itself is rarely the problem. What stalls projects is underestimating the integration cost, the work of bridging an extracted service back to the system it came from, especially across language boundaries where a hand-written HTTP client and DTO layer has to be built and maintained indefinitely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Is replatforming the same thing as legacy modernization?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Replatforming is one approach within legacy modernization, not the whole category. It means moving an existing system to new infrastructure without changing the code, which is lower risk than re-architecting but doesn't address the underlying architectural constraints that usually caused problems in the first place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. How long does an incremental modernization effort typically take compared to a full rewrite?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It depends entirely on the number of extractions and their complexity, but the more relevant difference is value delivery. A full rewrite delivers nothing until it ships, often years later. Incremental extraction under the Strangler Fig pattern delivers value continuously, with each extracted service live and useful as soon as it's cut over.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. What is an Anti-Corruption Layer and when does a team actually need one?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's a translation layer between a legacy system and a new service that converts legacy domain concepts into the new service's domain model. A team needs one when the legacy and target domain models genuinely diverge, different field semantics or status representations for the same underlying concept, since skipping it lets legacy constraints leak directly into the new codebase.&lt;/p&gt;

</description>
      <category>modernization</category>
      <category>ai</category>
      <category>webdev</category>
      <category>devops</category>
    </item>
    <item>
      <title>gRPC vs REST: A Decision Guide for Backend Teams</title>
      <dc:creator>Justin</dc:creator>
      <pubDate>Thu, 24 Sep 2026 11:21:09 +0000</pubDate>
      <link>https://dev.to/jusjustin/grpc-vs-rest-a-decision-guide-for-backend-teams-19lk</link>
      <guid>https://dev.to/jusjustin/grpc-vs-rest-a-decision-guide-for-backend-teams-19lk</guid>
      <description>&lt;h2&gt;
  
  
  &lt;strong&gt;TL;DR&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;REST runs on HTTP/1.1 with JSON and no enforced schema; it's the default for external-facing APIs and interfaces still taking shape. gRPC runs on HTTP/2 with Protobuf and a required contract, built for high-throughput internal services and latency-sensitive workloads like inference pipelines. The real tradeoff is speed and strictness versus universal compatibility, but neither protocol solves the integration maintenance work that piles up as services multiply, work that can eat 30-40% of backend engineering time. This guide breaks down when each protocol fits and what to do about the cost neither one addresses.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Getting Started: What REST and gRPC Actually Are&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Every service in a distributed system needs a way to talk to another one, and for most teams, that decision defaults to REST without much thought. It's familiar, it runs over plain HTTP, and any client- a browser, a script, another team's service- can call it without installing anything special first. gRPC is the alternative most teams reach for once performance or contract strictness becomes a real constraint on the system, not just a theoretical concern raised in a design review.&lt;/p&gt;

&lt;p&gt;REST is built on HTTP/1.1, the same protocol your browser uses to load a webpage. A service exposes its data as resources at URLs, callers interact using standard HTTP verbs like GET and POST, and data moves back and forth as JSON, plain readable text. Here's what a typical call looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;response = await client.post(
    "http://inference-service/api/v1/inference",
    json={"model": "llama-3", "prompt": prompt, "max_tokens": 512}
)
response.raise_for_status()
return response.json()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any HTTP client can make that call, no generated code, no special tooling, nothing beyond a standard library. That universality is REST's core strength: a new engineer, a third-party partner, or a browser-based frontend can start calling the API immediately.&lt;/p&gt;

&lt;p&gt;The tradeoff shows up on the other side of that convenience. Nothing enforces what shape the response has to take, so nothing stops the provider from renaming max_tokens to max_new_tokens in the next release. The calling service doesn't find out until production, usually as a silent null value where a number should be, or a KeyError that surfaces hours after the actual change shipped.&lt;/p&gt;

&lt;p&gt;gRPC starts from the opposite direction: it requires a contract before any code gets written.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The contract is a .proto file, which defines every method, field, and data type the service interface will expose.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A compiler called protoc reads that file and generates typed stubs from it, in whatever language the calling service uses.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Once those stubs exist, the calling code calls a generated method as it would any other function in its own codebase, rather than building a request by hand.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If a provider renames a field in the .proto, the generated stub changes to match, and the calling code fails to compile immediately, catching the break before it reaches production.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the core tradeoff in practice: REST gives you universal compatibility with a loose, self-enforced contract; gRPC gives you a strict, compiler-enforced contract in exchange for real setup cost on both sides.&lt;/p&gt;

&lt;p&gt;Two other differences are worth knowing before the next section gets more technical:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Transport:&lt;/strong&gt; gRPC runs over HTTP/2 instead of HTTP/1.1, which brings multiplexing, multiple requests traveling over a single connection at once, instead of REST's one-request-per-connection default.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Payload format:&lt;/strong&gt; gRPC encodes messages as Protobuf, a compact binary format a computer parses quickly but a human can't read directly, instead of the plain JSON text REST uses, which anyone can open and read without special tooling.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;How the Two Protocols Differ at a Technical Level&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Section 2 covered what each protocol is. This section covers what that difference actually costs or saves once a system is running in production, across four dimensions that matter most day-to-day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transport speed under load.&lt;/strong&gt; HTTP/2 gives gRPC multiplexing, header compression, and persistent connections, none of which HTTP/1.1 offers by default. In practice, that means fewer TCP handshakes and lower per-request overhead. For a single request, the difference is negligible. For a service handling hundreds of concurrent requests, like an inference endpoint under real traffic, that gap becomes measurable in p99 latency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Payload size on the wire.&lt;/strong&gt; JSON is verbose by design; every field name gets repeated in every message. Protobuf, gRPC's binary format, strips that repetition out. A typical Protobuf payload runs 3-10x smaller than the equivalent JSON. At low call volumes, that difference doesn't register. At high throughput, agent orchestration loops, inference pipelines, tool execution chains firing dozens of calls per request, it compounds fast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Contract enforcement.&lt;/strong&gt; This is the schema question from Section 2, restated in terms of what breaks and when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;With REST, schema enforcement is optional. Teams that want it add OpenAPI specs or contract testing on top, extra tooling that has to be maintained separately from the API itself.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;With gRPC, the .proto file is the enforced contract by default. There's no separate tooling to bolt on, the compiler does the enforcement automatically.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The practical difference: a REST contract break surfaces as a runtime bug someone has to trace back to its source. A gRPC contract break surfaces as a compile error, before the code ever ships.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Browser and client compatibility.&lt;/strong&gt; REST works natively in every browser and every HTTP client without exception. gRPC doesn't, because gRPC depends on HTTP/2 trailers, and the browser fetch API doesn't support them. Getting gRPC to work from a browser requires a proxy layer called gRPC-Web to translate between the two. That's not a minor inconvenience; it's a hard technical constraint, and it's the main reason gRPC stays scoped to internal service-to-service communication for most teams rather than reaching all the way to the frontend.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0m84bad5l58sytcxby5z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0m84bad5l58sytcxby5z.png" alt="Graftcode" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Put together, the pattern across all four dimensions is consistent: gRPC trades setup cost and reduced compatibility for speed and strictness, REST trades speed and strictness for universal reach and zero setup. Neither wins outright, they're suited to different jobs, which is exactly what the next two sections walk through.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;Why REST Is Still the Right Call for External-Facing APIs&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;REST's biggest advantage isn't speed or elegance, it's that it asks nothing of the caller. Any client that speaks plain HTTP can talk to a REST API without installing anything, generating any code, or agreeing to any contract in advance. That low bar is exactly why REST stays the right default in a specific, fairly consistent set of situations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Public-facing APIs&lt;/strong&gt; are the clearest case. A browser, a mobile app, a third-party integration, a CLI tool, any of them can call a REST API over plain HTTP with no stubs to generate and no special setup required first. If the API has consumers outside the team that built it, REST remains the default for good reason, nothing else offers that same zero-friction reach.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rapidly evolving interfaces&lt;/strong&gt; favor REST for a different reason. Early-stage products, exploratory internal services, APIs that still change weekly, all of these move faster without a .proto maintenance cycle attached to every change. When the schema itself isn't stable yet, the overhead of formally enforcing it is friction the team doesn't need to carry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Human-readable debugging&lt;/strong&gt; is easy to undervalue until it's the thing saving an afternoon. A REST call gone wrong is inspectable immediately in curl, Postman, or browser devtools, no extra step required. A Protobuf payload needs a deserializer and the right .proto file just to make sense of it. For a team onboarding a new engineer, or debugging a cross-service issue under pressure, that difference is felt directly.&lt;/p&gt;

&lt;p&gt;REST is the right fit when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The API has external consumers, browsers, third parties, or mobile clients&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The interface is still being shaped and changes frequently&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Broad tooling compatibility matters more than raw performance&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The team needs fast onboarding and easy debugging over strict guarantees&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One caveat worth flagging: REST's lack of enforced schema is manageable at small scale, but the manual discipline required to prevent contract drift grows right alongside the number of services and consumers. Teams that start on REST often end up investing in OpenAPI specs, contract testing, and SDK generation anyway, doing by hand what gRPC would have enforced automatically from day one.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;When gRPC Is the Right Call for Internal, High-Throughput Services&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Adopting gRPC means paying real setup cost before the first call ever goes out, writing the .proto file, generating stubs, and rerunning that generation step every time the interface changes. That investment pays off in a specific kind of system: one with high call volume, close latency tracking, and a team that controls both ends of the connection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;High-throughput internal services&lt;/strong&gt; are the primary case. An inference service fielding thousands of requests per minute, a tool execution pipeline inside an agent loop, a feature store queried by several downstream services, these are workloads that actually feel the difference between JSON over HTTP/1.1 and Protobuf over HTTP/2. Smaller payloads, persistent connections, and multiplexing don't matter much at low volume, but they compound fast at scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Polyglot service meshes&lt;/strong&gt; benefit from gRPC's code generation model specifically. When a Python orchestrator, a Go inference service, and a Java data pipeline all need to share one interface, the .proto file generates consistent typed stubs in each language from a single source of truth. No language can silently misinterpret a field another language set, because the contract is generated, not hand-written per language.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Latency-sensitive workloads&lt;/strong&gt; are where gRPC's HTTP/2 transport shows up most visibly, and a multi-tool agent loop is the clearest current example. Picture an orchestrator calling three services per request: a retrieval service, a code execution service, and a summarization service.&lt;/p&gt;

&lt;p&gt;With REST, each service call opens its own connection, one after another:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;async with httpx.AsyncClient() as client:
    retrieval = await client.post("http://retrieval-service/retrieve", json={...})
    execution = await client.post("http://execution-service/execute", json={...})
    summary = await client.post("http://summary-service/summarize", json={...})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With gRPC, all three calls run in parallel over persistent HTTP/2 channels instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;with futures.ThreadPoolExecutor(max_workers=3) as executor:
    r = executor.submit(retrieval_stub.Retrieve, RetrieveRequest(...), timeout=10.0)
    e = executor.submit(execution_stub.Execute, ExecuteRequest(...), timeout=10.0)
    s = executor.submit(summary_stub.Summarize, SummarizeRequest(...), timeout=10.0)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At 20 tool calls in a single agent run, even 5ms saved per hop adds up to 100ms off the total response time. That's not a marginal gain, it's the kind of difference that shows up directly in both user-perceived latency and infrastructure cost at scale.&lt;/p&gt;

&lt;p&gt;gRPC is the right fit when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Services are internal, and the team owns both the client and the server&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Call volume is high enough that payload size and connection overhead actually matter&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Multiple languages need to share one strict, enforced interface contract&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Streaming is part of the design, token streaming, live status updates, bidirectional flows&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;An agent loop is calling several downstream services in parallel per request&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Integration Maintenance Cost That Accumulates Regardless of Protocol&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Picking a protocol answers the transport question. It doesn't answer what happens after that: the ongoing work of keeping every service integration in sync as the system grows. Writing HTTP clients, keeping DTO schemas aligned, and managing stub regeneration, together these can consume 30-40% of backend engineering time, and the cost compounds with every service added.&lt;/p&gt;

&lt;p&gt;With REST, every integration means writing and maintaining an HTTP client by hand, wrapping request construction, error handling, and response parsing around each service the team calls.&lt;/p&gt;

&lt;p&gt;With gRPC, the stub handles transport automatically, but the .proto maintenance cycle becomes the team's responsibility instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Every time the inference service changes its interface:
protoc --python_out=. --grpc_python_out=. inference.proto
# Commit the regenerated stubs
# Update every calling service that imports them
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here's what interface drift looks like when that step gets skipped. A provider renames max_tokens to max_new_tokens in the .proto. Without regenerating stubs, the calling code fails silently in production. After regenerating, it fails loudly at compile time, TypeError: unexpected keyword, caught before it ever ships.&lt;/p&gt;

&lt;p&gt;Both paths land on the same underlying problem: keeping a calling service in sync with its provider is manual work, and that work scales with the number of services in the system, not with how complex any individual integration is.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Where Graftcode Fits In&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Section 6 reached the same conclusion for both protocols: neither removes the integration work sitting on top of the transport decision. Graftcode is built to address that layer directly, not by replacing REST or gRPC, but by sitting alongside whatever's already running.&lt;/p&gt;

&lt;p&gt;Instead of writing an HTTP client or running protoc, a calling service installs a Graft, a strongly-typed interface generated for that specific provider, through its normal package manager:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pip install --index-url https://grft.dev/your-project-id inference-service@1.0.0
pip install --index-url https://grft.dev/your-project-id retrieval-service@1.0.0
pip install --index-url https://grft.dev/your-project-id summary-service@1.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The three-service agent loop from Section 5, the one that needed either sequential REST calls or a manually parallelized gRPC stub setup, becomes a direct function call to each service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def run_agent_loop(query: str, prompt: str, text: str) -&amp;gt; dict:
    retrieval = RetrievalService.retrieve(query=query)
    inference = InferenceService.run_inference(model="llama-3", prompt=prompt, max_tokens=512)
    summary = SummaryService.summarize(text=text)
    return {"retrieval": retrieval, "inference": inference, "summary": summary}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No HTTP client, no DTO, no stub regeneration step. If a provider changes its interface, the change surfaces as a package update, caught at compile time rather than at runtime, the same protection gRPC offers, without the .proto cycle REST and gRPC both require the team to maintain by hand.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkpe7x7y0bg0i5vnhcufs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkpe7x7y0bg0i5vnhcufs.png" alt="Graftcode" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The routing itself is controlled by GraftConfig, which lives inside the calling service, not in a separate gateway or sidecar. It decides whether a given call runs in-process, never leaving the calling service at all, or as a remote call to the deployed provider, and that choice is set through an environment variable, a config file, or directly on the Graft itself. Switching a service from local development to a remote deployed dependency doesn't require touching application code.&lt;/p&gt;

&lt;p&gt;On performance, Graftcode's own Performance Lab ran a large-payload test, 5,000 data points per call, all three approaches on the same .NET runtime over HTTP/2, network latency excluded. Graftcode completed in 22ms, gRPC unary calls in 53ms, and REST in 1,245ms, putting Graftcode 98.2% faster than REST on that workload with a fraction of the CPU overhead. According to Graftcode, removing the HTTP client and stub layer entirely also reduces token usage for AI coding assistants by 30-60% on integration-heavy services, since there's no protocol boilerplate left for the assistant to parse and hold in context.&lt;/p&gt;

&lt;p&gt;Worth being direct about the constraint: Graftcode is still in Alpha, and primitive wrapper types like Date, Guid, and TimeSpan aren't supported yet, they need to be passed as string or int instead. That's the kind of limitation worth checking against current documentation before it factors into an adoption decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;A Practical Decision Framework for Backend Teams&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Protocol decisions rarely happen in isolation. They depend on who's calling the API, how much the interface is still changing, and what the team can realistically keep maintained. Three scenarios cover most real cases:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A SaaS platform with a public API and third-party integrations&lt;/strong&gt; should default to REST. Browsers and third-party clients need zero setup to start consuming it, and that reach outweighs whatever performance gain switching protocols might offer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An ML inference service called by several internal services&lt;/strong&gt; fits gRPC well. The interface is stable, call volume is high, latency is tracked closely, and the team controls both ends of the connection, which is exactly the profile Section 5 laid out. The .proto maintenance cost is worth paying for the strict contract and Protobuf's performance at scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A microservices team with a steadily growing number of internal services&lt;/strong&gt; is often looking at the wrong variable. If meaningful sprint time is going to integration code, HTTP clients, DTO sync, stub regeneration, the protocol choice isn't the actual bottleneck. This is the scenario Sections 6 and 7 speak to directly: the Performance Lab benchmark from Section 7 puts real numbers behind it, Graftcode at 22ms against gRPC's 53ms and REST's 1,245ms on the same workload. At 200k RPS on Azure Standard_D16s_v5, Graftcode's own estimate puts that gap at roughly \$1.64B in annual compute savings, a figure worth verifying against current published data before repeating it as settled fact.&lt;/p&gt;

&lt;p&gt;The signals that point toward each option, side by side:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Consumers:&lt;/strong&gt; external and browser-facing points to REST, internal-only points to gRPC or Graftcode&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Schema stability:&lt;/strong&gt; still evolving points to REST, stable and enforced points to gRPC or Graftcode&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Call volume:&lt;/strong&gt; moderate points to REST, high-throughput points to gRPC, very high with minimal CPU overhead points to Graftcode&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Languages involved:&lt;/strong&gt; single language or standard HTTP clients points to REST, polyglot with generated stubs points to gRPC, polyglot with no stubs needed points to Graftcode&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Integration overhead:&lt;/strong&gt; manageable points to REST, growing with service count points to gRPC, already consuming real team time points to Graftcode&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Where Should You Start&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Choosing between REST and gRPC comes down to who's calling the API and how much performance and contract strictness actually matter for that specific service, REST for external-facing APIs and evolving interfaces, gRPC for internal, high-throughput systems where the team controls both ends. But that choice only answers the transport question. The integration maintenance work sitting on top of it, hand-written HTTP clients, DTO sync, stub regeneration, accumulates regardless of which protocol a team picks, and it's the cost that keeps growing quietly while the protocol debate gets all the attention.&lt;/p&gt;

&lt;p&gt;For teams where that integration overhead is already visible in sprint velocity, Graftcode addresses that layer directly without requiring a rip-and-replace of what's already running. It installs alongside existing REST or gRPC services, one service boundary at a time, with compile-time contract enforcement and routing controlled through GraftConfig rather than hand-maintained client code. Full technical detail and the Performance Lab benchmarks are at graftcode.com, and the monolith-to-microservices use case is worth a look for teams currently mid-migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;FAQs&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Can gRPC and REST coexist in the same microservices architecture?&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;Yes, and most production systems already do this. External-facing APIs stay on REST for broad client compatibility, while internal service-to-service communication moves to gRPC for performance and contract enforcement. An API gateway at the edge typically handles the translation between the two.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. What's the actual performance difference between REST and gRPC at high call volumes?&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;At low volumes, the gap is small enough to ignore. At high throughput, thousands of requests per second, Protobuf's binary encoding and HTTP/2's multiplexing produce measurable latency and CPU differences, with gRPC benchmarks consistently showing lower p99 latency under load, particularly for the small, frequent payloads typical of internal service calls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Is gRPC a realistic option for browser-based frontend applications?&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;Not natively. Browsers don't support the HTTP/2 trailers gRPC depends on, so reaching a browser requires a proxy layer called gRPC-Web to translate between the two. For most teams, that added infrastructure makes REST the practical default for anything browser-facing, with gRPC scoped to internal communication.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. How does Graftcode handle routing compared to gRPC's service discovery?&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;GraftConfig, which lives inside the calling service, controls where a remote call goes and points to the Graftcode Gateway running for that provider service. Load balancing itself still sits at the infrastructure layer, the same Kubernetes service or load balancer a gRPC or REST deployment would use, since Graftcode replaces the integration code layer, not the infrastructure underneath it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Can Graftcode be adopted alongside an existing REST or gRPC service during a migration?&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;Yes, this is one of the more common ways teams bring it in. A team can extract one service, run its Graftcode Gateway for that service, and set the calling code to run in-process locally while routing remotely in staging and production, without changing the application code. Existing REST or gRPC services keep running unchanged elsewhere in the system.&lt;/p&gt;

</description>
      <category>grpc</category>
      <category>webdev</category>
      <category>decisionguide</category>
      <category>ai</category>
    </item>
    <item>
      <title>Agencies that market to software engineers and developers</title>
      <dc:creator>Justin</dc:creator>
      <pubDate>Mon, 16 Mar 2026 10:07:10 +0000</pubDate>
      <link>https://dev.to/jusjustin/how-to-hire-a-developer-marketing-agency-for-my-devtool-startup-9cg</link>
      <guid>https://dev.to/jusjustin/how-to-hire-a-developer-marketing-agency-for-my-devtool-startup-9cg</guid>
      <description>&lt;p&gt;Sure you must have heard how marketing to developers is notoriously difficult. Not because developers are impossible to reach — but because they ignore most marketing.&lt;/p&gt;

&lt;p&gt;Engineers care about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;solving problems&lt;/li&gt;
&lt;li&gt;learning new tools&lt;/li&gt;
&lt;li&gt;seeing working examples&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This means developer marketing looks very different from typical SaaS marketing.&lt;/p&gt;

&lt;p&gt;Instead of ads and slogans, you usually need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;tutorials&lt;/li&gt;
&lt;li&gt;documentation&lt;/li&gt;
&lt;li&gt;architecture guides&lt;/li&gt;
&lt;li&gt;open-source examples&lt;/li&gt;
&lt;li&gt;deep technical blogs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Over the past few years, a handful of agencies have emerged that specialize in this space.&lt;/p&gt;

&lt;p&gt;Here are a few worth knowing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Infrasity&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="//infrasity.com"&gt;Infrasity &lt;/a&gt;focuses on developer infrastructure companies and SaaS platforms.&lt;/p&gt;

&lt;p&gt;Their work tends to revolve around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;developer tutorials&lt;/li&gt;
&lt;li&gt;technical blogs&lt;/li&gt;
&lt;li&gt;integration guides&lt;/li&gt;
&lt;li&gt;DevOps and infrastructure topics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What makes them interesting is that much of the content is written by engineers, which helps avoid the “marketing fluff” problem that developers hate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Draft.dev&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Draft.dev connects companies with developer writers.&lt;/p&gt;

&lt;p&gt;They produce technical articles covering topics like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;programming languages&lt;/li&gt;
&lt;li&gt;APIs&lt;/li&gt;
&lt;li&gt;cloud infrastructure&lt;/li&gt;
&lt;li&gt;security&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many devtool startups use them to scale technical blog production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Animalz&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Animalz is well known in the SaaS world for high-quality long-form content.&lt;/p&gt;

&lt;p&gt;They’re not specifically developer-focused, but they’ve worked with many technical companies and understand complex SaaS products.&lt;/p&gt;

&lt;p&gt;Omniscient Digital&lt;/p&gt;

&lt;p&gt;Omniscient focuses heavily on SEO-driven content strategy.&lt;/p&gt;

&lt;p&gt;For developer products targeting search traffic around tutorials or comparisons, this approach can work well.&lt;/p&gt;

&lt;p&gt;Developer relations consultancies&lt;/p&gt;

&lt;p&gt;Some companies also partner with DevRel-focused agencies, which handle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;community engagement&lt;/li&gt;
&lt;li&gt;developer programs&lt;/li&gt;
&lt;li&gt;conferences&lt;/li&gt;
&lt;li&gt;technical advocacy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This can complement content-focused marketing efforts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Final thoughts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The best developer marketing rarely feels like marketing. It looks more like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;documentation&lt;/li&gt;
&lt;li&gt;tutorials&lt;/li&gt;
&lt;li&gt;engineering blog posts&lt;/li&gt;
&lt;li&gt;open-source examples&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Agencies that understand this difference tend to produce the content developers actually trust.&lt;/p&gt;

</description>
      <category>developers</category>
      <category>marketing</category>
      <category>softwaredevelopment</category>
      <category>softwareengineering</category>
    </item>
  </channel>
</rss>
