<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>Benjamin Cane — #Bengineering</title>
    <link>https://bencane.com</link>
    <description>Practical engineering notes from Benjamin Cane, usually shared on LinkedIn first and kept readable on the open web.</description>
    <language>en</language>
    
      <image>
        <url>https://bencane.com/assets/images/bengineering-card.png</url>
        <title>Benjamin Cane — #Bengineering</title>
        <link>https://bencane.com</link>
      </image>
    
    
      <lastBuildDate>Wed, 30 Sep 2026 24:00:00 GMT</lastBuildDate>
    
    
      
      
      
      
        
      
      <item>
        <title>One of my favorite aspects of gRPC isn’t the performance. It’s the well-defined contracts Protobuf provides.</title>
        <link>https://bencane.com/posts/2026-09-30-protobuf-contract-management/</link>
        
        <guid isPermaLink="false">https://bencane.com/posts/2026-09-30/</guid>
        <dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Benjamin Cane</dc:creator>
        <description><![CDATA[<p>One of my favorite aspects of gRPC isn’t the performance.
It’s the well-defined contracts Protobuf provides.</p>
<p>I’ve shared a lot about the benefits of gRPC, and most of that focuses on what makes it fast and reliable.
One of the biggest benefits has nothing to do with performance.
It’s maintainability and consistency.</p>
<p>But that maintainability and consistency rely heavily on how well you manage the Protobuf contract.</p>
<h2>Protobuf Contracts Can Be Mismanaged</h2>
<p>Protobuf and gRPC naturally gravitate toward well-managed contracts, but like anything, we can still overcomplicate or mismanage things.</p>
<p>The most common mistake I’ve seen is every application keeping its own copy of the <code>.proto</code> file and generating code from it independently.</p>
<p>When changes happen, they have to be made across every copy of the <code>.proto</code> file.
Those copies can drift very quickly.</p>
<p>If nobody introduces a conflicting change, it might work for a while.
But conflicts happen eventually.</p>
<p>Imagine two teams independently modify their copies of the <code>.proto</code> file.</p>
<p>One adds:</p>
<pre><code class="language-protobuf">string email = 3;
</code></pre>
<p>Another adds:</p>
<pre><code class="language-protobuf">string phone_number = 3;
</code></pre>
<p>Now both have assigned different meanings to the same field number.
And because both assigned a string to field 3, this doesn’t fail in predictable ways.</p>
<p>One application could send an email address, while another is expecting a phone number.
Hopefully, you find that drift in testing, not in production.</p>
<p>The easiest way to avoid this scenario is to maintain a single source of truth for the contract.</p>
<h2>One Contract, One Source of Truth</h2>
<p>At sufficient scale, you may need a schema registry or platform that manages Protobuf/gRPC contracts.
But if you only have a few gRPC contracts to manage, I have a pretty simple solution that works well.</p>
<p>First, keep the <code>.proto</code> files in one source-controlled home.
That’s the source of truth.</p>
<p>Make any updates there via pull requests.
Changes get reviewed and tracked, and because everyone is modifying the same <code>.proto</code> file, it’s much easier to catch conflicts.</p>
<p>Second, generate code from that contract.
Bonus points if you automate this as part of the CI process.
Then commit and publish the generated code as versioned libraries for the languages you support.</p>
<p>With this approach, applications don’t maintain their own copies of the <code>.proto</code> definition or generate code independently.
They import a library/package.</p>
<p>You don’t have to worry about which version of the <code>.proto</code> file was used.
You don’t have to worry about the <code>protoc</code> version or which plugin options were used.
It becomes a normal software dependency.</p>
<p>All you need to worry about is which version you are using.</p>
<p>Contract changes follow a pretty simple process:</p>
<p><code>Pull Request -&gt; Review -&gt; Merge -&gt; Code Generation -&gt; Publish -&gt; Dependency Update -&gt; Profit.</code></p>
<h2>Consistency Becomes the Easy Path</h2>
<p>The best part of this approach is that consistency becomes the easy path.</p>
<p>If five services consume the same gRPC service, five teams don’t need to understand how to generate the client correctly.</p>
<p>They don’t need to manage a <code>.proto</code> file.
They don’t need to install the right version of <code>protoc</code>.
They import a library/package.</p>
<p>This approach also makes contract adoption visible.</p>
<p>If version <code>v1.8.0</code> adds a new capability, you can easily see which applications have upgraded and which are still using older versions.
This approach turns contract management into dependency management.
And every team has to manage software dependencies anyway.</p>
<h2>You Still Need to Manage Compatibility</h2>
<p>While this approach makes contract and implementation management much easier, it’s not foolproof.</p>
<p>Breaking changes can still be introduced.
Consumers can still sit on old versions.</p>
<p>Adding new fields is generally straightforward.
But removing or changing existing fields requires coordination (and should be avoided if possible).</p>
<p>It’s also important to recognize that producers/servers and consumers/clients evolve independently.
If you only added a field or two, and the consumer doesn’t need it, running an older version might be fine.
But over time, letting consumers run significantly different versions can cause functionality to drift across applications.</p>
<p>Good dependency management and automation can make this much easier to manage, though.</p>
<p>Overall, if you keep backward compatibility in mind, this approach works well.
Still, if you let those practices slip, you’ll find yourself coordinating version changes across multiple applications, which is time-consuming and often challenging.</p>
<h2>Final Thoughts</h2>
<p>I’ve seen entire projects lose months because two teams understood the same contract differently.</p>
<p>Protobuf/gRPC doesn’t make that impossible.
But if you structure things well, it can make it much harder.
And that is one of my favorite things about Protobuf/gRPC.</p>
<p>It’s fast and efficient, which is great.
But a well-managed <code>.proto</code> contract makes integration between systems consistent and much easier to maintain.
Don't treat a contract as documentation; treat it as code.
Code that can be versioned, distributed, and managed like any other dependency.</p>
]]></description>
        <pubDate>Wed, 30 Sep 2026 24:00:00 GMT</pubDate>
        
          <media:content url="https://bencane.com/assets/images/posts/2026-09-30-protobuf-contract-management.png" medium="image" />
        
      </item>
    
      
      
      
      
        
      
      <item>
        <title>Message queues are great for handing off work, but how do you know the work actually happens?</title>
        <link>https://bencane.com/posts/2026-09-23-message-completion/</link>
        
        <guid isPermaLink="false">https://bencane.com/posts/2026-09-23/</guid>
        <dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Benjamin Cane</dc:creator>
        <description><![CDATA[<p>Message queues are great for handing off work, but how do you know the work actually happens?</p>
<p>Message queues and brokers are great solutions for many use cases.
They can provide excellent resiliency and strong guarantees that a message gets where it needs to go.</p>
<p>But I find those delivery guarantees can sometimes create a false sense of resiliency.
Because delivering a message and completing the work that message represents are two different things.</p>
<h2>Peeling Back the Everything Breaks Onion</h2>
<p>A message broker might reliably deliver a message to a consumer.
Great.
But what if that consumer receives the message and fails while processing it?</p>
<p>What if it processes the message but fails while calling a downstream service?
What if the consumer succeeds, but the overall business process doesn’t?</p>
<p>Message delivery isn’t the same thing as business-process completion.
For best-effort messages, maybe that’s fine.
For mission-critical work, I generally want some mechanism to know whether that work actually completed.</p>
<h2>Tracking Completion</h2>
<p>One way is to track the operation and expect an application-level acknowledgment.</p>
<p>Let’s explore a simplified approach:</p>
<p>A producer publishes:</p>
<p><code>ProcessPayment:123456789</code></p>
<p>At the same time, it records in a local DB:</p>
<p><code>123456789 -&gt; PENDING</code></p>
<p>The consumer processes the message and, once the work completes, publishes:</p>
<p><code>PaymentProcessed:123456789</code></p>
<p>When the producer receives that message, it updates the local DB:</p>
<p><code>123456789 -&gt; COMPLETE</code></p>
<p>Now instead of simply knowing the original message was delivered, we have a record of the work actually completing.</p>
<h2>What If the Completion Never Arrives?</h2>
<p>If operation <code>123456789</code> stays <code>PENDING</code>, it’s tempting to assume the work failed.
But that’s not necessarily true.</p>
<p>Maybe the consumer failed.
Maybe a downstream dependency failed.
Or maybe the producer failed to process the acknowledgment.
Either way, we haven’t confirmed operation completion, and what happens next depends on the use case.</p>
<p>You may be able to retry, or you may need to query the downstream system and validate the outcome.
You might need to wake someone up, or you do nothing because the message was best-effort anyway.</p>
<p>The important part is that the uncertainty is detectable and visible, which means you can decide what to do about it.</p>
<h2>Final Thoughts</h2>
<p>Of course, many message brokers already have acknowledgment mechanisms.
Those are useful, and depending on how they are used, might provide similar assurances.</p>
<p>But many implementations only tell you a message has been delivered or consumed.
If the consumer hands the work off to another system, or starts a larger business process, the broker doesn’t understand whether the work ultimately completed.</p>
<p>That uncertainty can be problematic for some use cases.</p>
<p>Of course, adding this additional layer of certainty comes with its own complexities.
It adds state management, more components in the flow, and more room for failure.</p>
<p>But for mission-critical work, guarantees are essential.
A broker can provide strong guarantees around message delivery, but your application is ultimately responsible for determining and exposing whether the work actually happened.</p>
]]></description>
        <pubDate>Wed, 23 Sep 2026 24:00:00 GMT</pubDate>
        
          <media:content url="https://bencane.com/assets/images/posts/2026-09-23-message-completion.png" medium="image" />
        
      </item>
    
      
      
      
      
        
      
      <item>
        <title>Some architecture principles should be rules. Others should be guidelines.</title>
        <link>https://bencane.com/posts/2026-09-16-architecture-rules-guidelines/</link>
        
        <guid isPermaLink="false">https://bencane.com/posts/2026-09-16/</guid>
        <dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Benjamin Cane</dc:creator>
        <description><![CDATA[<p>Not all architecture principles should be treated the same.</p>
<p>Some should be rules.
Others should be guidelines.</p>
<p>Explicitly calling out the difference gives teams enough structure to maintain the architectural vision, while still giving engineers room to make good decisions.</p>
<h2>Why Architecture Principles Matter</h2>
<p>I’ve helped envision, modernize, transform, and fix many platforms throughout my career.
One of the most valuable things you can do early in that process is to define a set of architectural principles.</p>
<p>Large transformations take time.
Along the way, different engineers and teams will make tons of technical decisions.</p>
<p>A good set of principles helps ground those decisions and ensures the platform you end up with still resembles the platform you originally envisioned.</p>
<p>But the platform doesn’t stop evolving when the project ends.</p>
<p>New requirements appear, new technologies emerge, constraints change, and individual engineers/architects come and go.
Your principles need to provide structure without preventing the architecture from evolving.</p>
<p>That’s why I like to separate them into two categories: Rules and Guidelines.</p>
<h2>Rules: These Must Remain True</h2>
<p>An architecture rule defines something that must remain true about the platform.</p>
<p>For example:</p>
<blockquote>
<p>“Data must be encrypted at rest and in transit.”</p>
</blockquote>
<p>Or:</p>
<blockquote>
<p>“Services within one cell cannot directly communicate with services in another cell.”</p>
</blockquote>
<p>These aren’t preferences; they are architectural constraints that engineers must design with rather than decide whether to follow.</p>
<h2>Guidelines: This Is How We Prefer to Build</h2>
<p>Architecture guidelines provide direction while leaving room for engineers to apply contextual judgment.</p>
<p>For example:</p>
<blockquote>
<p>“Services should be stateless whenever practical.”</p>
</blockquote>
<p>There may be good reasons for a service to maintain state, and that’s ok.</p>
<p>The guideline establishes the preferred approach, but engineers can deviate when the problem context justifies it.</p>
<h2>Why You Need Both</h2>
<p>I’ve run into problems when architecture principles are treated entirely as guidelines or entirely as rules.</p>
<p>If everything is a guideline, eventually nothing is a rule.</p>
<p>Someone may have a legitimate reason to deviate.
Another team will see that implementation and copy it.</p>
<p>Now the exception becomes precedent, and over time the original principle slowly disappears.</p>
<p>But making everything a rule creates the opposite problem.</p>
<p>Eventually, you’ll encounter a scenario you didn’t anticipate when the principles were written.
Now you need an exception.</p>
<p>Once you establish a process for exceptions, that process can quickly become the path of least resistance.
The rule slowly becomes a guideline anyway.</p>
<p>Or your platform becomes so rigid with rules that engineers can't meet changing needs.</p>
<p>Explicitly defining which principles are rules and which are guidelines gives you the best of both worlds.
Rules protect the architectural characteristics you aren’t willing to compromise.
Guidelines give engineers room to make contextual decisions when circumstances change.</p>
<h2>This Matters Even More With Coding Agents</h2>
<p>Smaller engineering teams have sometimes gotten away without explicitly documenting principles.
Engineers learn them through code reviews, architecture discussions, and tribal knowledge.</p>
<p>Coding agents don’t have years of tribal knowledge.
They only understand the design constraints they can access.</p>
<p>If we want coding agents to make increasingly larger implementation decisions, we need to explicitly tell them where they have engineering freedom and where they don’t.</p>
<p>Writing down your architecture principles and making them easily accessible to engineers and agents alike is becoming even more important.</p>
<p>Tell an engineer who understands your architecture to retrieve account data.
That engineer will instinctively know they should leverage the owning service's API rather than accessing its database directly.</p>
<p>Ask a coding agent to fetch the same data; it might try to connect to the database directly, breaking the “microservices must not share databases” architecture rule.</p>
<p>Ask a coding agent to do something without context, and you’ll get exactly what you asked for, whether it violates your architecture principles or not.</p>
<p>If the agent doesn’t know the rules, it can’t follow them.</p>
<h2>Final Thoughts</h2>
<p>Architecture principles have always been valuable.
Writing them down is more important now than ever.</p>
<p>But don’t just define how you want your platform to look at the end.
Be explicit about which principles are rules and which are guidelines.</p>
<p>Good architecture principles should tell engineers and agents where they have freedom to make decisions and where they don’t.</p>
]]></description>
        <pubDate>Wed, 16 Sep 2026 24:00:00 GMT</pubDate>
        
          <media:content url="https://bencane.com/assets/images/posts/2026-09-16-architecture-rules-guidelines.png" medium="image" />
        
      </item>
    
      
      
      
      
        
      
      <item>
        <title>Mishandling concurrency is one of the most common root causes of software bugs</title>
        <link>https://bencane.com/posts/2026-09-09-mishandling-concurrency/</link>
        
        <guid isPermaLink="false">https://bencane.com/posts/2026-09-09/</guid>
        <dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Benjamin Cane</dc:creator>
        <description><![CDATA[<p>Of all the software bugs I’ve seen, mishandling concurrency is one of the most common root causes.</p>
<p>Sharing objects without proper synchronization across threads in Java.
Timing issues across multiple workers in Python.
Race conditions and deadlocked goroutines in Go.</p>
<p>Concurrency can be difficult to get right regardless of the programming language.</p>
<p>But for high-performance and high-scale systems, it’s often a necessary complexity.</p>
<p>Concurrency lets our software process multiple tasks at the same time.
But the moment you do, you introduce an entirely new set of problems.</p>
<p>Some of those problems can hide for years, like landmines waiting for the right time.</p>
<h2>Shared Data Gets Complicated</h2>
<p>Let’s take a simple example: a shared map.</p>
<p>If multiple threads or goroutines are reading from the map concurrently, things may be perfectly fine.
But what happens when one starts modifying the map while another is reading or also writing?
Now you need synchronization.</p>
<p>In Go, for example, unsynchronized concurrent access to a map where at least one goroutine is writing can result in runtime failures or data races.
So you need something to coordinate access: a mutex, channels, or a design that avoids sharing objects altogether.</p>
<p>It might sound obvious to use something like a mutex to solve this problem, but sometimes, these bugs don’t exist when the code is originally written.</p>
<p>Maybe the map was initially scoped to a single goroutine.
Six months later, someone else adds a background process that updates it periodically.
Suddenly, something that was perfectly safe is not.</p>
<p>The code didn’t fundamentally change; the way it was accessed did.</p>
<h2>Timing Creates Another Problem</h2>
<p>Shared data isn’t the only concurrency challenge.
Sometimes it’s ordering.</p>
<p>Imagine you need to process a sequence of related messages.
Message 1 arrives, then message 2.</p>
<p>If you distribute those messages across multiple threads, goroutines, or even servers, arrival order no longer guarantees processing order.</p>
<p>Maybe the worker processing Message 2 has less of a work backlog.
Maybe the server it lands on is slightly faster.</p>
<p>Maybe Message 1 gets delayed by a database call.</p>
<p>What if Message 2 finishes first?
Does it matter?
Maybe, maybe not.</p>
<p>But if Message 2 depends on something Message 1 was supposed to do, you now have a correctness problem.</p>
<p>In cases like this, related work needs to be serialized or partitioned so that operations that require ordering follow the same funnel.</p>
<h2>It Works Until It Doesn’t</h2>
<p>What makes concurrency bugs nasty is that your code can be wrong, but still work.</p>
<p>Take the shared map example.
Run one request at a time during testing, and you’ll never see this problem.
Run a few requests concurrently, and you still might not see it.</p>
<p>The timing has to be just right.</p>
<p>You could have a concurrency bug sitting in production for years without knowing it exists.
Then traffic increases, or CPU utilization changes, or a dependency gets just a bit slower.
Something that has worked millions of times before starts failing.</p>
<p>Try again, and everything is fine.
Not because you fixed something; timing was just different.</p>
<p>Concurrency bugs are notoriously hard to reproduce and diagnose for this reason.</p>
<h2>So How Do You Find Them?</h2>
<p>Functional tests aren't enough; you need tests and tooling that deliberately create or detect concurrency behavior.</p>
<p>In Go, one of the easiest things you can do is run your tests with the race detector by adding the <code>-race</code> flag.</p>
<p>It can identify concurrent memory access that might trigger data races.
Linters can also help catch other concurrency mistakes.</p>
<p>But one technique I’ve found surprisingly effective is performance testing.</p>
<p>Normally we think of performance tests as a way to answer how many requests a service can handle, or latency at a certain throughput.
But performance tests are also a great way to find correctness problems.</p>
<p>Why?
Because they create concurrency.</p>
<p>Thousands or millions of operations increase the chances of those rare timing conditions occurring.</p>
<p>Running functional performance (<code>go run -race</code>) or benchmark (<code>go test -race</code>) tests with Go’s data race detector won’t produce trustworthy performance numbers.
Still, it can be incredibly effective at exposing concurrency bugs.</p>
<h2>Final Thoughts</h2>
<p>Concurrency is difficult to get right, even in languages like Go that give us excellent tools to work with it.</p>
<p>But for many systems, it’s a necessary complexity.
You can’t always avoid concurrency.
You need to build guardrails around it.</p>
<p>Use data race detection, static analysis, and test systems with many requests at once.
Put these checks into your build pipelines so engineers don’t have to remember to run them.</p>
<p>Because concurrency bugs aren't always obvious at first, they can sit there quietly for years, waiting for just the right timing to create havoc.</p>
]]></description>
        <pubDate>Wed, 09 Sep 2026 24:00:00 GMT</pubDate>
        
          <media:content url="https://bencane.com/assets/images/posts/2026-09-09-mishandling-concurrency.png" medium="image" />
        
      </item>
    
      
      
      
      
        
      
      <item>
        <title>At what point does better performance stop being worth it?</title>
        <link>https://bencane.com/posts/2026-09-02-performance-complexity-tradeoff/</link>
        
        <guid isPermaLink="false">https://bencane.com/posts/2026-09-02/</guid>
        <dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Benjamin Cane</dc:creator>
        <description><![CDATA[<p>At what point does better performance stop being worth it?</p>
<p>When thinking about performance, especially for low-latency and high-scale applications, I don’t believe in “good is good enough.”
I think it’s important to leave yourself room to grow and keep looking for improvements.</p>
<p>But there is a point where gaining performance requires significantly more complexity.
And sometimes that tradeoff isn’t worth it.</p>
<p>I recently had a conversation with a team considering <code>gRPC</code> to send events from one system to another.</p>
<p>We are talking about very high volumes of relatively small requests.</p>
<p>They had two important requirements:</p>
<ul>
<li>Events needed to move quickly</li>
<li>The sender needed to know each event was successfully received</li>
</ul>
<p>Given their other constraints, traditional <code>HTTP</code> APIs and message brokers weren’t a great fit.</p>
<p>But I posed an interesting question: should they use unary <code>gRPC</code> calls or bidirectional streams?</p>
<h2>🚀 The Faster Option</h2>
<p>Their initial plan was to implement bidirectional streaming.</p>
<p>The client could continuously send events across a stream, and the server could asynchronously send acknowledgments as it accepted events.
It’s not a bad design.</p>
<p>From a performance perspective, it’s a great design.</p>
<p>But there is a catch.</p>
<p>With this approach, the application needs to track what was sent, what is still in-flight, what was acknowledged, and what needs to be retried.
And that’s if all goes well.
What happens if the stream disconnects?
Which events were processed, and which were lost?
How do you avoid duplicates?</p>
<p>All solvable problems, but they all add complexity to the application.</p>
<h2>🤔 What If You Don’t Need The Fastest Option?</h2>
<p>Now let’s compare with unary <code>gRPC</code>.</p>
<p>From an application perspective, you send a request, and you get a response.
Success or failure is tied directly to that request.</p>
<p>You don’t need an in-flight cache mapping acknowledgments back to events because the request/response lifecycle already does that for you.</p>
<p>It’s a much simpler implementation.
And importantly, unary <code>gRPC</code> is still fast.</p>
<p>You still get the benefits of <code>gRPC</code>: Protocol Buffers, persistent <code>HTTP/2</code> connections, and multiplexing requests across those connections.
But you do give up some performance compared with a well-designed streaming implementation.</p>
<p>But the real question is, did they need that additional performance?</p>
<h2>⚖️ There Are Also Operational Tradeoffs</h2>
<p>Streaming changes how traffic is distributed.
A long-lived stream is established with a particular backend and remains there for the life of that connection.</p>
<p>If you have three clients, six servers, and each client opens one stream, you could end up actively sending traffic to only three servers or less.</p>
<p>It’s solvable: open multiple streams, build a stream pool, balance the work across them, and manage the connection lifecycle during failures.
Solvable, but more complexity for the team to implement.</p>
<p>In contrast, with unary requests, infrastructure like a service mesh can distribute requests across available backends without the application needing to manage pools, connection lifecycles, etc.</p>
<p>Send a request to the service mesh; it will distribute it across the six servers.
Scaling and failure handling become far less complex.</p>
<h2>📈 Performance Has Diminishing Returns</h2>
<p>The team will test unary operations for their application.</p>
<p>If unary operations meet their throughput and latency requirements with plenty of headroom for growth, they face an interesting choice.</p>
<p>Bidirectional streaming could give them even more performance.
But what are they buying with that additional performance?</p>
<p>If they need it, the complexity is justified.
If they don’t, they are taking on additional development, testing, and operational complexity for performance they won't use.</p>
<p>That’s a bad tradeoff.</p>
<h2>🧐 Final Thoughts</h2>
<p>I dislike the “good is good enough” mentality.
Continuously improving your platforms is an essential practice for any platform that cares about performance.</p>
<p>But performance has a cost.
Few of us are building systems just to be fast.
We’re also building them to be reliable and maintainable.
And eventually, someone has to operate that system in the middle of the night when something unexpected happens.</p>
<p>If the simpler solution meets your needs while leaving enough headroom to scale for tomorrow, it may be the best option.
The question to ask yourself is: what is the right balance of simplicity and performance?</p>
]]></description>
        <pubDate>Wed, 02 Sep 2026 24:00:00 GMT</pubDate>
        
          <media:content url="https://bencane.com/assets/images/posts/2026-09-02-performance-complexity-tradeoff.png" medium="image" />
        
      </item>
    
      
      
      
      
        
      
      <item>
        <title>Sometimes good engineering looks like over-engineering</title>
        <link>https://bencane.com/posts/2026-08-26-good-engineering-looks-like-over-engineering/</link>
        
        <guid isPermaLink="false">https://bencane.com/posts/2026-08-26/</guid>
        <dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Benjamin Cane</dc:creator>
        <description><![CDATA[<p>Sometimes good engineering looks like over-engineering.</p>
<p>I was recently putting together a talk that walks through some architecture and engineering decisions I’ve made over the last eight-plus years when something stood out to me.</p>
<p>Without context, many of those decisions probably look like over-engineering.</p>
<h2>🤔 An Example: Operational Toggles</h2>
<p>In several mission-critical payment applications, we follow a simple philosophy:</p>
<p>If a dependency isn’t required to process customer traffic, we should be able to turn it off.</p>
<p>For example, metrics.</p>
<p>Metrics are important, but they aren’t more important than processing transactions.
If something goes wrong with our metrics implementation, we want to be able to turn off metrics while continuing to process traffic.</p>
<p>The same applies to out-of-band management messages and data updates.
We typically use message brokers for this, and if there is an issue with that integration or a bug in the message handlers, we want the ability to stop consuming those messages without taking down the application.</p>
<p>Why?</p>
<p>Because we want fine-grained operational control over our platform.
A non-critical capability shouldn’t be able to take down the critical path.</p>
<h2>🤯 Isn’t That Over-Engineering?</h2>
<p>Maybe.</p>
<p>These operational toggles add complexity to the system: flags that need to be managed, tests that need to be written and executed, and operational procedures for failures that might never happen.</p>
<p>For many applications, it’s probably not worth it.</p>
<p>If a metrics library causes a memory leak in an application that does batch processing, restarting to release the memory is perfectly acceptable.</p>
<p>If a message handler crashes an application that is automatically restarted, it might be ok depending on the application.
Not great, but if requests are retried and nothing truly fails, no harm, no foul.</p>
<p>Not every application needs the complexity of these operational toggles.</p>
<p>But when an application crash could cause impact, when the system is mission-critical, and every failure is potentially impactful, the equation changes.</p>
<h2>🧠 Context Determines Everything</h2>
<p>For the batch and non-critical application examples, these operational toggles could easily be considered over-engineering.</p>
<p>But for a mission-critical, always-on, large-scale platform, when a situation occurs and these operational toggles are used, they become incredibly valuable.</p>
<p>The above examples are not hypothetical.</p>
<p>A change in metrics causes a spike in memory usage.
An unsafe type assertion in a data sync handler from a message broker causes an application to crash.</p>
<p>These are real scenarios, scenarios that I’ve seen in mission-critical systems.
Because these operational toggles existed, these scenarios were not disastrous or even inconvenient.</p>
<p>These operational toggles let us turn off the cause of an issue.
More importantly, they gave us time.
Time to understand what happened, build and properly test a fix, and validate that the fix actually solved the root problem.</p>
<p>All without rushing a change to production while the platform was unstable.</p>
<h2>🧐 Final Thoughts</h2>
<p>If you look at the way we use operational toggles, it could easily feel like over-engineering.
But building critical systems means planning for failure scenarios even if they are unlikely.</p>
<p>Sometimes that additional complexity might look unnecessary.
But when something goes wrong, you’re thankful it’s there.</p>
]]></description>
        <pubDate>Wed, 26 Aug 2026 24:00:00 GMT</pubDate>
        
          <media:content url="https://bencane.com/assets/images/posts/2026-08-26-operational-toggles.png" medium="image" />
        
      </item>
    
      
      
      
      
        
      
      <item>
        <title>AI makes code cheap to create, not cheap to own</title>
        <link>https://bencane.com/posts/2026-08-19-ai-code-cheap-to-create-not-cheap-to-own/</link>
        
        <guid isPermaLink="false">https://bencane.com/posts/2026-08-19/</guid>
        <dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Benjamin Cane</dc:creator>
        <description><![CDATA[<p><code>AI</code> has made code cheap to create, but more code means more overhead.</p>
<p>Before coding agents, creating more code cost time and effort because engineers had to write it.</p>
<p>That naturally encouraged us to reuse code as much as possible: libraries, frameworks, keeping implementations concise, and thinking twice before rebuilding functionality that already existed.</p>
<p>Coding agents have dramatically reduced that natural cost.</p>
<p>Today, generating another implementation costs almost nothing.
But you still have to maintain it.</p>
<h2>🤖 Just Let the Agent Build It</h2>
<p>There’s a growing mindset that because agents can generate code quickly, there’s less reason to worry about reuse.</p>
<p>Why spend time finding an existing library when an agent can recreate the functionality in seconds?
And initially, that might feel faster, better.</p>
<p>But let’s say you have six applications that all need to validate email addresses against your internal requirements.</p>
<p>Instead of creating a common package, you let your coding agent implement email validation directly in each application.
As long as the prompt is the same, it’s fine, right?
Not necessarily.
You can easily end up with six implementations, all slightly different.</p>
<p>Then you find a bug.</p>
<h2>🪳 Creation is Cheap, Maintenance Isn’t</h2>
<p>Fixing the application with the bug is easy.</p>
<p>Tell the agent about the bug; it fixes the implementation, maybe it adds some tests, done.</p>
<p>What about those five other implementations?
Do they also have this bug?</p>
<p>Maybe, maybe not.
Because each application has a different implementation, they may have the same bug or different bugs.</p>
<p>You’ve essentially found yourself in a situation where, depending on the application, an email might work, or it might not.
Maybe it works in three of the six, or two.
What happens if that email works for a registration page, but not the login page?</p>
<p>Not a great user experience.</p>
<h2>🧱 Reuse Still Matters</h2>
<p>Compare that to using a shared package.
You write it once, everyone uses the same package.</p>
<p>You find a bug, fix it once, add tests, and release a new version.</p>
<p>Every instance using that new version has that bug fixed.</p>
<p>Now your suite of applications is consistent, and consistency matters most.
Rejecting a valid email isn’t great, but if you’re consistent about it, that's a much better experience than it sometimes working and sometimes not.</p>
<p>That consistency is much easier to manage if the implementation is in one reusable library.</p>
<h2>⚖️ Don’t Go DRY Crazy</h2>
<p>Don’t use the above example to go to the other extreme, where every similar three lines of code turns into a shared library.
Breaking everything into its own little shared library creates overhead costs, too.</p>
<p>It’s important to strike a balance between reusable and duplicative.</p>
<p>If you’re writing something with complex or nuanced behavior that’s needed across multiple applications, or more importantly, needs to behave consistently across them, a shared library is probably a good idea.</p>
<p>If it’s simple, situationally unique, and unlikely to change, don’t bother.</p>
<h2>🧐 Final Thoughts</h2>
<p>The cost of creating code has changed significantly.
But the cost of managing it hasn’t.</p>
<p>Testing it, patching it, securing it, and keeping implementations consistent all become more expensive as the amount of code you own grows.</p>
<p><code>AI</code> has made it incredibly cheap to create more code.
Don’t confuse cheap to generate with cheap to own.</p>
]]></description>
        <pubDate>Wed, 19 Aug 2026 24:00:00 GMT</pubDate>
        
          <media:content url="https://bencane.com/assets/images/posts/2026-08-19-ai-code-ownership.png" medium="image" />
        
      </item>
    
      
      
      
      
        
      
      <item>
        <title>“We can’t run locally” is usually a design smell</title>
        <link>https://bencane.com/posts/2026-08-13-run-locally-design-smell/</link>
        
        <guid isPermaLink="false">https://bencane.com/posts/2026-08-13/</guid>
        <dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Benjamin Cane</dc:creator>
        <description><![CDATA[<p>“We can’t run locally” is usually a design smell.</p>
<p>I’m a believer that, as an engineer, you should be able to run your software locally.
But I hear it often: “We can’t run locally because of some reason.”</p>
<p>Sometimes it’s valid.
There are architectures and platforms out there that prevent running locally.
But more often than not, when it comes to backend distributed systems, it’s a design or implementation decision that nobody has challenged.</p>
<h2>Why This Matters</h2>
<p>Everyone these days is focused on speeding up software delivery.
Today, that generally means coding agents, <code>AI</code> tooling, and code generation.</p>
<p>But writing code faster doesn’t matter much if validating a one-line change takes an hour.</p>
<p>The speed of software delivery is often dictated by validation, not code generation.</p>
<h2>The Problem with Shared Dev Environments</h2>
<p>Many teams still rely on shared development environments as their primary way of validating changes.</p>
<p>That process usually looks something like this:</p>
<ul>
<li>Make a change</li>
<li>Commit the change</li>
<li>Push the branch</li>
<li>Wait for a build</li>
<li>Wait for a deployment</li>
<li>Run tests</li>
</ul>
<p>If everything goes well, you can validate your change.
If a mistake was made, or something doesn’t work right, you have to fix it and start over.</p>
<p>The result is a slow feedback loop, which can sometimes influence behavior.</p>
<p>The more friction there is to validate a change, the larger that change becomes.
If testing takes a long time, you will naturally focus on making sure everything is perfect before spending time validating.</p>
<h2>Local Validation Influences Good Behavior</h2>
<p>Compare the above process to one that uses a locally running service.</p>
<ul>
<li>Make a change</li>
<li>Build</li>
<li>Start the service</li>
<li>Run tests</li>
</ul>
<p>The process is dramatically shorter and, more importantly, faster.
This encourages engineers to make smaller changes, test more frequently, iterate their implementations, and find mistakes earlier.</p>
<p>This all results in better software.</p>
<h2>The Excuses</h2>
<p>“My service depends on too many other services.”</p>
<p>Run the services you own locally.
Mock the services you don’t.</p>
<p>“I need a database or message broker.”</p>
<p>Run those locally too.
There’s very likely a Docker container for each of them.</p>
<p>“We use cloud services.”</p>
<p>This one can be harder, but emulators exist for some services.
Other services might have a Dockerized open-source alternative.
For those that don’t, you could mock them.</p>
<p>There will always be exceptions: mainframes, specialized hardware, or unique managed services.</p>
<p>But I find many teams jump straight to “we can’t run locally” before they’ve seriously explored how they could.</p>
<h2>Running Local Doesn’t Mean Everything</h2>
<p>You don’t need to recreate your entire production environment on a laptop, though if you can, go for it.</p>
<p>The goal is to validate your change without depending on a shared or non-local environment that takes forever to test against.</p>
<p>Local development isn’t about recreating production.
It’s about creating enough of the environment to validate your change.
If that means turning off some functionality or creating mock services, it might be worth it.</p>
<h2>Why This Matters Even More Now</h2>
<p>Validating changes quickly is becoming more important as teams adopt coding agents.
An agent can generate code in seconds.</p>
<p>But if every iteration with your coding agent requires committing, pushing, building, deploying, and waiting, the feedback loop becomes the bottleneck.</p>
<h2>Final Thoughts</h2>
<p>Agents make mistakes.
Humans make mistakes.</p>
<p>The faster you can validate a change, the faster you can correct those mistakes.
That’s why local execution isn’t just a convenience.</p>
<p>It’s one of the most effective ways to shorten the feedback loop.</p>
]]></description>
        <pubDate>Thu, 13 Aug 2026 24:00:00 GMT</pubDate>
        
          <media:content url="https://bencane.com/assets/images/posts/2026-08-13-run-locally.png" medium="image" />
        
      </item>
    
      
      
      
      
        
      
      <item>
        <title>To make a service more stable, eliminate dependencies</title>
        <link>https://bencane.com/posts/2026-08-06-eliminate-dependencies/</link>
        
        <guid isPermaLink="false">https://bencane.com/posts/2026-08-06/</guid>
        <dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Benjamin Cane</dc:creator>
        <description><![CDATA[<p>To make a service more stable, eliminate dependencies.</p>
<p>One of the simplest reliability rules I’ve learned is this:
Every dependency is another way for your service to fail.</p>
<h2>Why Dependencies Matter</h2>
<p>Every service has dependencies.</p>
<ul>
<li>Databases</li>
<li>Caches</li>
<li>Configuration services</li>
<li>Secrets managers</li>
<li>Logging pipelines</li>
<li>Tracing backends</li>
</ul>
<p>All of these dependencies can fail, and when they do, the typical service will fail with them.</p>
<p>The more dependencies a service has, the more failures it inherits.
Every dependency adds features, but it also adds failure modes.</p>
<p>Reliability is often about deciding which dependencies are actually worth the tradeoff.</p>
<h2>Why Edge Systems Tend to Be Dependency-Light</h2>
<p>I recently wrote about how systems closest to the customer carry the greatest responsibility for availability.</p>
<p>This is one reason edge systems tend to be dependency-light.</p>
<p>Load balancers, API gateways, and routers are responsible for availability.</p>
<p>Every dependency added to these systems creates another opportunity to take down the entire platform.
So they tend to avoid dependencies whenever possible.</p>
<h2>Eliminating Dependencies Isn’t Always Necessary</h2>
<p>Sometimes removing a dependency entirely isn’t practical.</p>
<p>A better question is:</p>
<ul>
<li>What happens if the dependency disappears?</li>
<li>Can the service continue operating?</li>
<li>Can it use cached data?</li>
<li>Can it fall back to a last-known-good configuration?</li>
<li>Can it degrade gracefully?</li>
</ul>
<p>If the answer is yes, you’ve significantly improved reliability even though the dependency still exists.</p>
<h2>A Real-World Example</h2>
<p>Take Envoy Proxy.
Envoy can receive configuration from an <code>xDS</code> service.
But it doesn’t call <code>xDS</code> for every request.</p>
<p>Instead:</p>
<ul>
<li>Configuration is fetched periodically</li>
<li>Stored in memory</li>
<li>Used locally during request processing</li>
</ul>
<p>If the <code>xDS</code> service becomes unavailable, Envoy continues routing traffic using its last known configuration.
The dependency still exists, but request processing no longer depends on its availability.</p>
<p>That’s a very different reliability model.</p>
<h2>Don’t Make Observability a Hard Dependency</h2>
<p>One of the most common mistakes I see is making observability a required dependency.</p>
<p>If your logging or tracing backend becomes unavailable, should customer traffic stop flowing?
No.</p>
<p>In most cases, observability should be a best effort.</p>
<p>Use asynchronous logging, buffering, and truncation policies so customer traffic continues to flow even when your observability platform is experiencing issues.</p>
<p>Operational visibility is important, but customer availability is more important.</p>
<h2>Final Thoughts</h2>
<p>You’ll never eliminate every dependency.
But you can eliminate unnecessary ones.</p>
<p>And for the remaining dependencies, you can design your service to survive failures.</p>
<p>The most reliable services aren’t dependency-free.
They’re designed to survive dependency failures.</p>
]]></description>
        <pubDate>Thu, 06 Aug 2026 24:00:00 GMT</pubDate>
        
          <media:content url="https://bencane.com/assets/images/posts/2026-08-06-eliminate-dependencies.png" medium="image" />
        
      </item>
    
      
      
      
      
        
      
      <item>
        <title>Caching isn’t hard. Some data is hard to cache</title>
        <link>https://bencane.com/posts/2026-07-30-caching-data-tradeoffs/</link>
        
        <guid isPermaLink="false">https://bencane.com/posts/2026-07-30/</guid>
        <dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Benjamin Cane</dc:creator>
        <description><![CDATA[<p>Caching isn’t hard.
Some data is hard to cache.</p>
<p>You’ve all heard the advice: “Avoid caching because caching is difficult to get right.”</p>
<p>I agree with part of that statement.
Caching can absolutely be difficult.
But I think the reality is more nuanced.</p>
<p>The difficulty level of caching depends heavily on the type of data you are caching.</p>
<h2>Not All Data Is Equal</h2>
<p>When engineers talk about caching complexity, they’re usually thinking about data that changes frequently and requires strong consistency.</p>
<p>That’s the hardest kind of data to cache.
Because you need to answer some difficult questions.</p>
<ul>
<li>How do you invalidate the cache?</li>
<li>How do you handle updates?</li>
<li>What happens when the cache population fails?</li>
<li>How stale is too stale?</li>
</ul>
<p>These problems are real, but they don’t apply to all data.</p>
<h2>Frequently Updated Data</h2>
<p>Some data changes constantly and has strict accuracy requirements.
Account balances, inventory counts, active orders, and similar records.
This is where caching is difficult.</p>
<p>Every stale read has a consequence, and in some cases, it’s not worth the complexity.</p>
<h2>Infrequently Updated Data</h2>
<p>Some data changes occasionally, once an hour, once a day, or even longer.
Configuration data, product catalogs, country codes, and similar records.</p>
<p>In these cases, a small amount of staleness might be fine.
If data changes hourly and your cache refreshes every few minutes, that’s often a reasonable tradeoff.
The less frequently data changes, the less sensitive you become to cache staleness.</p>
<p>Caching is much easier with infrequently updated data.</p>
<h2>Immutable Data</h2>
<p>Immutable data is the easiest to cache.
Because it never changes.
Ledger entries, event records, receipts, and other immutable records.</p>
<p>Once immutable data is loaded into a cache, cache invalidation largely disappears as a problem.
Cache complexity for immutable data is more about balancing performance and hit-miss ratios.</p>
<p>Most of the time, immutable data is an ideal candidate for caching.</p>
<h2>The Real Question</h2>
<p>Instead of asking, “Should I cache?”
Ask yourself, “How often does this data change?
How accurate does it need to be?”</p>
<p>The answers will give you an idea of how complex caching will be.</p>
<h2>Final Thoughts</h2>
<p>Caching isn’t inherently hard.</p>
<p>Maintaining correctness for frequently changing data is hard.</p>
<p>The more stable the data, the less complex the caching needs to be.</p>
<p>Like most things in system design, you can’t just take all-or-none answers like “always cache” or “never cache.”
It’s important to understand the trade-offs, constraints, and consistency requirements of the data you’re working with.</p>
]]></description>
        <pubDate>Thu, 30 Jul 2026 24:00:00 GMT</pubDate>
        
          <media:content url="https://bencane.com/assets/images/posts/2026-07-30-caching-data-tradeoffs.png" medium="image" />
        
      </item>
    
      
      
      
      
        
      
      <item>
        <title>The closer to the edge, the more stable a platform must be</title>
        <link>https://bencane.com/posts/2026-07-23-edge-platform-stability/</link>
        
        <guid isPermaLink="false">https://bencane.com/posts/2026-07-23/</guid>
        <dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Benjamin Cane</dc:creator>
        <description><![CDATA[<p>The closer to the edge, the more stable a platform must be.</p>
<p>The closer a component is to the customer, the greater its responsibility for keeping the entire platform available, even when everything behind it is having a bad day.</p>
<h2>Not All Services Carry the Same Reliability Burden</h2>
<p>Let’s consider a typical platform.</p>
<p>Customer -&gt; Load Balancer -&gt; API Gateway -&gt; Orchestrator -&gt; Microservices -&gt; Database</p>
<p>Every layer has a different job.
But every layer also has a different level of responsibility for resiliency.</p>
<p>As you move toward the customer, that responsibility increases.</p>
<h2>Deep Services Focus on Business Logic</h2>
<p>At the deepest layers of the platform, services are usually focused on business capabilities.</p>
<p>They process orders, transfer money, manage inventory, and store data.</p>
<p>These services often have databases, business rules, stateful operations, and multiple dependencies.</p>
<p>Resiliency matters, but it’s often focused on correctness.</p>
<p>If a database call fails:</p>
<ul>
<li>Should the transaction roll back?</li>
<li>Should the service fail over?</li>
<li>Should a compensating transaction occur?</li>
</ul>
<p>These services are primarily concerned with business outcomes.</p>
<h2>The Middle Layers Absorb Failures</h2>
<p>Move up a layer, and you often find orchestrators and workflow services.
These components coordinate work across multiple services.</p>
<p>If one service fails, the orchestrator may retry, execute fallback logic, trigger compensating actions, or roll back a workflow.
Their job is not just executing business logic, it’s ensuring execution succeeds despite failures.</p>
<h2>The Edge Exists to Protect Everything Behind It</h2>
<p>At the edge, things change.</p>
<p>Load balancers and API gateways are often stateless, dependency-light, highly available, and extremely fast.</p>
<p>Why?</p>
<p>Because their primary responsibility is availability.
Everything behind them is allowed to fail, and they absorb as much of that failure as possible.</p>
<p>They:</p>
<ul>
<li>Route around failures</li>
<li>Shed load</li>
<li>Fail over traffic</li>
<li>Enforce timeouts</li>
<li>Apply retries</li>
<li>Protect backend systems</li>
</ul>
<p>The edge isn’t just resilient for itself.
It’s resilient on behalf of everything behind it.</p>
<h2>Final Thoughts</h2>
<p>The deepest services in a platform should be focused on business logic.
The edge should be focused on availability.</p>
<p>The more failures your edge can absorb, the less every downstream service needs to care.
That’s why the closer you get to the customer, the more stable the platform must become.</p>
]]></description>
        <pubDate>Thu, 23 Jul 2026 24:00:00 GMT</pubDate>
        
          <media:content url="https://bencane.com/assets/images/posts/2026-07-23-edge-platform-stability.png" medium="image" />
        
      </item>
    
      
      
      
      
        
      
      <item>
        <title>Sometimes the most resilient thing a system can do isn’t retry</title>
        <link>https://bencane.com/posts/2026-07-16-compensating-transactions/</link>
        
        <guid isPermaLink="false">https://bencane.com/posts/2026-07-16/</guid>
        <dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Benjamin Cane</dc:creator>
        <description><![CDATA[<p>Sometimes the most resilient thing a system can do isn’t retry.</p>
<p>Most resiliency discussions focus on retries, timeouts, and circuit breakers.</p>
<p>But some of the most important resiliency patterns happen after the failure.</p>
<p>That’s where compensating transactions come in.</p>
<h2>Resiliency Is About Recovery</h2>
<p>A common mistake is thinking resiliency means preventing failures.</p>
<p>Failures are inevitable.</p>
<p>Networks fail.
Services crash.
Messages get lost.
Requests time out.</p>
<p>True resiliency is accepting that failures will occur and having a plan to recover when they do.</p>
<p>Sometimes recovery means correcting actions that may already have occurred.</p>
<h2>A Real-World Example</h2>
<p>Have you ever swiped your card at a store, received a charge notification, only to have the terminal report an error and later discover that the charge was nowhere to be found?</p>
<p>What happened?</p>
<p>The point-of-sale terminal sent an authorization request.</p>
<p>But before it received a response, something failed: a network issue, a timeout, or a problem somewhere in the payment flow.</p>
<p>Now the terminal doesn’t know whether the transaction succeeded or failed.</p>
<p>The authorization request may have reached the card issuer and been processed, or it may not have.</p>
<p>Rather than risk leaving the customer incorrectly charged, the terminal sends a second transaction: a reversal.</p>
<h2>The Compensating Transaction</h2>
<p>That reversal is a real-world example of a compensating transaction.</p>
<p>Its purpose is simple:</p>
<p>Undo the effects of a previous action if that action completed successfully.
If the original authorization never happened, nothing changes.</p>
<p>If it did happen, the reversal corrects it.
Instead of determining exactly what happened, the system performs a corrective action.</p>
<h2>Beyond Payments</h2>
<p>Compensating transactions show up in many real-world distributed systems:</p>
<ul>
<li>Releasing inventory after a failed order</li>
<li>Refunding a payment after a fulfillment failure</li>
<li>Canceling a reservation when verification times out</li>
</ul>
<p>The pattern is always the same: something failed, and the system takes a corrective action.</p>
<h2>Final Thoughts</h2>
<p>When engineers think about resiliency, they often focus on preventing failures.
But distributed systems fail in unexpected ways.</p>
<p>Sometimes the most resilient thing a system can do isn’t retry.
It’s correct the mistake and move forward.</p>
]]></description>
        <pubDate>Thu, 16 Jul 2026 24:00:00 GMT</pubDate>
        
          <media:content url="https://bencane.com/assets/images/posts/2026-07-16-compensating-transactions.png" medium="image" />
        
      </item>
    
      
      
      
      
        
      
      <item>
        <title>Should retries and timeouts live in your application or your service mesh?</title>
        <link>https://bencane.com/posts/2026-07-09-platform-vs-application-resiliency/</link>
        
        <guid isPermaLink="false">https://bencane.com/posts/2026-07-09/</guid>
        <dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Benjamin Cane</dc:creator>
        <description><![CDATA[<p>Should retries and timeouts live in your application or your service mesh?</p>
<p>This debate comes up constantly.</p>
<p>Should resiliency live in the platform components, or should the application own it?</p>
<p>Like most things in distributed systems, the answer is: It depends.</p>
<h2>Infrastructure Understands Traffic</h2>
<p>Service meshes, API gateways, and load balancers are great at handling generic resiliency concerns.</p>
<p>Things like:</p>
<ul>
<li>Connection timeouts</li>
<li>Automatic retries</li>
<li>Circuit breakers</li>
<li>Request-level failover</li>
</ul>
<p>The advantage is obvious.
You remove complexity from the application and apply resiliency consistently across services.</p>
<p>For many scenarios, this is the right answer.</p>
<h2>Infrastructure Doesn’t Understand Intent</h2>
<p>The challenge is that platform services only understand traffic.
They don’t understand why the request is made.</p>
<p>They don’t know whether a request is reading customer profile data, reserving inventory, transferring money, or uploading cat videos.</p>
<p>To the service mesh, they’re all just requests.
Some requests might even have custom timeouts and retries configured.
Most of the time, that’s fine.</p>
<p>Sometimes, it’s not.</p>
<h2>Context-Aware Resiliency</h2>
<p>Some resiliency decisions require application context.</p>
<p>Consider a financial transaction.
Should a timeout trigger a retry?
Maybe.</p>
<p>Does the request have an idempotency key?
If yes, blindly retrying might be safe.
If not, things become more complicated.</p>
<p>Did the request reach the downstream system?
Was it partially processed?</p>
<p>Do you need a compensating transaction before retrying?</p>
<p>At this point, the retry is no longer a networking decision.
It’s a business decision.
And business decisions belong in the application.</p>
<h2>The Real Answer</h2>
<p>The best architectures usually use both approaches.</p>
<p>Use platform resiliency whenever the decision can be made without application context.
But when correctness depends on understanding the request itself, move that logic into the application.</p>
<p>There should be a strong preference toward offloading complexity when possible.
Just don’t offload decisions that require business context.</p>
<h2>Final Thoughts</h2>
<p>I often see teams over-index on platform-level resiliency.
And while reducing application complexity is valuable, it isn’t free.</p>
<p>Infrastructure understands traffic and request characteristics.
Applications understand business intent.</p>
<p>When resiliency decisions depend on business intent, they belong in the application.</p>
]]></description>
        <pubDate>Thu, 09 Jul 2026 24:00:00 GMT</pubDate>
        
          <media:content url="https://bencane.com/assets/images/posts/2026-07-09-platform-vs-application-resiliency.png" medium="image" />
        
      </item>
    
      
      
      
      
        
      
      <item>
        <title>Need to migrate from one database to another without downtime?</title>
        <link>https://bencane.com/posts/2026-07-02-dual-writes-database-migration/</link>
        
        <guid isPermaLink="false">https://bencane.com/posts/2026-07-02/</guid>
        <dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Benjamin Cane</dc:creator>
        <description><![CDATA[<p>Need to migrate from one database to another without downtime?</p>
<p>Dual writes are one approach that deserves more attention.</p>
<p>Most database migrations fall into one of a few buckets:</p>
<ul>
<li>Export and import</li>
<li>Replication between two databases</li>
<li>Services specifically built to synchronize data</li>
</ul>
<p>All of those approaches can work well.
But sometimes you need both databases active while gradually migrating traffic from one to the other.</p>
<p>That’s where dual writes become interesting.</p>
<h2>What Are Dual Writes?</h2>
<p>The idea is exactly what it sounds like.
Every write is sent to both databases.</p>
<p>Insert a row?
Write it twice.</p>
<p>Update a record?
Update it twice.</p>
<p>Delete something?
Yup, delete it twice.</p>
<p>The goal is to keep the old and new databases synchronized while both are active.</p>
<h2>How It Works</h2>
<p>The most common approach is implementing dual writes directly in the application.</p>
<p>Instead of maintaining one database connection pool, the application maintains two and executes write operations against both systems.</p>
<p>In some cases, infrastructure can help as well.</p>
<p>For example, Envoy supports request mirroring patterns that can be useful when migrating certain technologies, such as Redis.</p>
<p>The implementation will vary, but the concept remains the same: each write is performed twice.</p>
<h2>The Hard Part: Failure Handling</h2>
<p>The hard part is not writing twice, but rather handling partial success.</p>
<p>What happens when Database A succeeds, but Database B fails?</p>
<p>Now the two databases disagree.</p>
<p>Do you retry?
Can the operation be safely retried?</p>
<p>Can you roll back the successful write?</p>
<p>Do you have locking or reconciliation mechanisms?</p>
<p>This is where dual writes become significantly more complex than they initially sound.</p>
<p>Writing twice is easy.
Maintaining correctness is the hard part.</p>
<h2>Where Dual Writes Work Best</h2>
<p>Dual writes tend to work best when:</p>
<ul>
<li>Updates occur frequently</li>
<li>Eventual consistency is acceptable</li>
<li>Reconciliation processes exist</li>
<li>Future updates naturally correct drift</li>
</ul>
<p>They become much harder in systems that require strict consistency guarantees for every operation.</p>
<h2>Why Teams Use Them</h2>
<p>Despite the complexity, dual writes enable a powerful migration approach.</p>
<p>You can:</p>
<ul>
<li>Introduce a new database platform or table</li>
<li>Keep it synchronized with the old platform</li>
<li>Gradually migrate read traffic</li>
<li>Eventually retire the old database</li>
</ul>
<p>All without requiring a large downtime window.</p>
<h2>Final Thoughts</h2>
<p>Like most migration strategies, dual writes are a tradeoff.</p>
<p>They add complexity.</p>
<p>But they also enable something extremely valuable: migrating between database platforms while the system remains live.</p>
<p>When downtime is not an option, dual writes can be one of the most powerful migration techniques available.</p>
]]></description>
        <pubDate>Thu, 02 Jul 2026 24:00:00 GMT</pubDate>
        
          <media:content url="https://bencane.com/assets/images/posts/2026-07-02-dual-writes-database-migration.png" medium="image" />
        
      </item>
    
      
      
      
      
        
      
      <item>
        <title>Glue Services: Part Two — Data Synchronization</title>
        <link>https://bencane.com/posts/2026-06-25-glue-services-data-synchronization/</link>
        
        <guid isPermaLink="false">https://bencane.com/posts/2026-06-25/</guid>
        <dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Benjamin Cane</dc:creator>
        <description><![CDATA[<p>Glue Services: Part Two — Data Synchronization.</p>
<p>I recently talked about using glue services (Anti-Corruption Layers) to isolate modern platforms from legacy integrations.</p>
<p>Today I want to talk about another type of glue service: data synchronization services.</p>
<h2>🗃️ The Real Modernization Problem</h2>
<p>One of the hardest parts of replacing a legacy platform is usually not the application itself.</p>
<p>It’s the data.</p>
<p>In a perfect world, you could: take downtime, export the database, import it into the new platform, and switch traffic.</p>
<p>Simple.</p>
<p>But large downtime windows are increasingly rare.
And big-bang migrations are risky enough that many organizations actively avoid them.</p>
<p>That means old and new systems often need to run side-by-side for a while.</p>
<p>Both platforms stay active.</p>
<p>Both serve customers.</p>
<p>Both need access to the same data.
This is where modernization becomes difficult.</p>
<h2>🔄 Keeping Two Systems in Sync</h2>
<p>When you are changing both the application and the underlying database, keeping data synchronized becomes difficult very quickly.</p>
<p>Especially when the two systems use different schemas, database technologies, data models, and update patterns.</p>
<p>You can’t simply export/import anymore.
You need continuous synchronization.</p>
<h2>👨🏻‍🤝‍👨🏾 Enter the Synchronization Service</h2>
<p>One useful pattern is to build a dedicated synchronization service whose sole responsibility is to keep data aligned across systems.</p>
<p>How synchronization works depends entirely on the platform.</p>
<p>I’ve used several approaches over the years.</p>
<h2>⚙️ Database Triggers</h2>
<p>One approach is to use database triggers or change-capture mechanisms in the legacy system.</p>
<p>When data changes, it’s detected, the synchronization service processes it, and the new platform is updated.</p>
<h2>📩 Event-Based Synchronization</h2>
<p>Another approach is to have the legacy platform emit events via a message broker.</p>
<p>The synchronization service consumes those events and updates the new database/platform accordingly.
This tends to work especially well when modernizing toward event-driven systems.</p>
<h2>⏳ Temporary Infrastructure</h2>
<p>The important thing to remember is that these synchronization services are usually temporary.
Their job is not to become a permanent platform.</p>
<p>Their purpose is to enable safe migration while reducing downtime and risk.</p>
<p>Once the migration is complete, the glue service disappears.</p>
<h2>⚠️ One Important Warning</h2>
<p>Bi-directional synchronization becomes extremely difficult very quickly.</p>
<p>With bi-directional synchronization, you have to solve:</p>
<ul>
<li>Conflict resolution</li>
<li>Consistency problems</li>
<li>Update ordering</li>
<li>Partial failures</li>
</ul>
<p>Whenever possible, avoid bi-directional synchronization.</p>
<p>In many cases, it is simpler to migrate the application layer first, then the database layer.</p>
<h2>🧐 Final Thought</h2>
<p>Modernization is rarely just replacing code.
A major challenge is safely bridging old and new systems during transition periods.</p>
<p>Synchronization glue services are often what make zero-downtime migrations possible.</p>
]]></description>
        <pubDate>Thu, 25 Jun 2026 24:00:00 GMT</pubDate>
        
          <media:content url="https://bencane.com/assets/images/posts/2026-06-25-data-synchronization-glue-services.png" medium="image" />
        
      </item>
    
      
      
      
      
        
      
      <item>
        <title>When modernizing legacy systems, don’t be afraid to build glue services</title>
        <link>https://bencane.com/posts/2026-06-18-glue-services-legacy-modernization/</link>
        
        <guid isPermaLink="false">https://bencane.com/posts/2026-06-18/</guid>
        <dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Benjamin Cane</dc:creator>
        <description><![CDATA[<p>When modernizing legacy systems, don’t be afraid to build glue services.</p>
<p>One of the biggest mistakes I see during modernization efforts is letting legacy integrations dictate the design of the new platform.</p>
<p>That usually leads to putting fresh paint on the same old house.
Rebuilding the same architecture with a newer tech stack.</p>
<h2>😴 The Dream vs. Reality</h2>
<p>The dream project is building a brand-new platform with no existing users, integrations, or constraints.</p>
<p>You can design everything “the right way” from day one.
But most real-world projects are not like that.</p>
<p>Most projects are modernization efforts.
And most modernization efforts are weighed down by existing integrations, legacy protocols, and operational dependencies.</p>
<p>Changing customer behavior and expectations is often harder than rebuilding the platform itself.</p>
<h2>🥲 The Common Mistake</h2>
<p>Many teams respond to this by centering their new platform on the old integration model.</p>
<p>If customers use XML over Message Brokers, the new platform may speak JSON, but it still inherits the event-driven constraints—even when they no longer make sense.</p>
<p>If the legacy system exchanges files, the new platform is usually heavily batch-based.</p>
<p>The problem is:</p>
<p><strong>Your modernization effort becomes constrained by the past.</strong></p>
<h2>🤯 A Better Approach: Glue Services</h2>
<p>The formal term for this pattern is an <em>Anti-Corruption Layer</em>.</p>
<p>Personally, I think “glue service” explains it better.
It makes the concept easier to understand.</p>
<p>Build the internal platform the way you actually want it designed.</p>
<p>Then build lightweight edge services that translate between legacy integrations and your modern platform.</p>
<ul>
<li>XML over Message Brokers? Use <code>gRPC</code> internally.</li>
<li>Files? Break them into APIs.</li>
<li>Long-lived <code>ISO8583</code> TCP connections? Terminate them at the edge and use <code>gRPC</code> + microservices behind them.</li>
</ul>
<p>The glue service has one responsibility: to translate between old and new worlds.</p>
<h2>🤔 Why This Matters</h2>
<p>These glue services give you two major advantages.</p>
<p>First, your internal architecture stays modern and optimized for current engineering practices.
Second, your customers and integrations do not need to migrate immediately.</p>
<p>That dramatically reduces modernization risk.
Temporary glue services can be huge modernization enablers.</p>
<p>But sometimes these glue services live forever, which is ok.</p>
<p>The important thing is that legacy integrations no longer constrain your modern platform.</p>
<h2>🔁 It Works at Both Ends</h2>
<p>This pattern applies both to inbound (clients calling your platform) and outbound (your platform calling legacy systems) integrations.</p>
<p>In many large systems, you’ll find glue services on both sides of the platform.</p>
<p>At the edge entering the system, and again leaving it.</p>
<h2>🧐 Final Thoughts</h2>
<p>Modernization is rarely just rewriting code.
A major challenge is safely bridging old and new systems during transition periods.</p>
<p>I’ve used this pattern many times.</p>
<ul>
<li>Breaking files into APIs</li>
<li>Translating <code>ISO8583</code> and <code>ISO20022</code> messages into Protobuf</li>
<li>Terminating long-lived TCP sessions at the edge of a <code>gRPC</code>-based microservices platform</li>
</ul>
<p>Glue services may not be glamorous, but they are one of the safest ways to modernize systems without dragging the past into the future.</p>
]]></description>
        <pubDate>Thu, 18 Jun 2026 24:00:00 GMT</pubDate>
        
          <media:content url="https://bencane.com/assets/images/posts/2026-06-18-glue-services.png" medium="image" />
        
      </item>
    
      
      
      
      
        
      
      <item>
        <title>Coding agents can’t see your architecture diagrams—fix that</title>
        <link>https://bencane.com/posts/2026-06-11-code-based-architecture-diagrams/</link>
        
        <guid isPermaLink="false">https://bencane.com/posts/2026-06-11/</guid>
        <dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Benjamin Cane</dc:creator>
        <description><![CDATA[<p>Coding agents can’t see your architecture diagrams—fix that.</p>
<p>I’ve been talking a lot about architecture documentation and how it helps both humans and agents.</p>
<p>From my experience, most teams spend 90% of their effort on diagrams and 10% on text.</p>
<p>Why? Because they are the fastest way to communicate a system:</p>
<p>Diagrams show how components interact, how data flows, and what depends on what.
They help humans understand complex systems quickly.</p>
<p>That’s why most architecture documentation leans heavily on diagrams.</p>
<h2>🤔 The Problem</h2>
<p>Most diagrams are images.
Images are great for humans, but not for agents.</p>
<p>Some agents can interpret images, but not reliably or consistently.
And even when an agent can interpret an image, it can’t reliably reason about it or keep it up to date.</p>
<h2>🧠 Make Diagrams Understandable</h2>
<p>You don’t need to move away from diagrams to embrace agents.
Just make them understandable to agents.</p>
<p>If diagrams are the most valuable part of your architecture documentation, make them readable as code.</p>
<h2>📝 Use Code-Based Diagrams</h2>
<p>Tools like Mermaid turn text into diagrams.</p>
<p>That means agents can read them, reason about them, and even keep them up to date.</p>
<p>A Mermaid diagram isn’t just documentation—it’s structured context.</p>
<h2>💡 Why This Matters</h2>
<p>When your diagrams are code:</p>
<ul>
<li>They live with your system</li>
<li>They evolve with changes (as long as you update them)</li>
<li>They can be versioned</li>
<li>They can be generated or modified by agents</li>
</ul>
<p>And most importantly, they become a living, usable context.</p>
<h2>🔄 Keep Them Up to Date</h2>
<p>Keeping architecture documentation up to date is always a pain.
Images make that even harder.</p>
<p>Code-based diagrams are much easier to keep up to date.
Especially when you use agents and the architecture sits next to your code.</p>
<p>When you make a change, direct the agent to update the architecture documentation as well.
Even if you don’t fully trust agents with code changes, architecture documentation is low risk.</p>
<p>Instead of letting it drift over time, let agents keep it up to date.</p>
]]></description>
        <pubDate>Thu, 11 Jun 2026 24:00:00 GMT</pubDate>
        
          <media:content url="https://bencane.com/assets/images/posts/2026-06-11-code-based-diagrams.png" medium="image" />
        
      </item>
    
      
      
      
      
        
      
      <item>
        <title>Most teams put low-level architecture in the wrong place</title>
        <link>https://bencane.com/posts/2026-06-04-component-architecture-in-codebase/</link>
        
        <guid isPermaLink="false">https://bencane.com/posts/2026-06-04/</guid>
        <dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Benjamin Cane</dc:creator>
        <description><![CDATA[<p>Most teams put low-level architecture in the wrong place—if they document it at all.</p>
<p>I recently wrote about keeping architecture documentation in Git and making it available to coding agents.</p>
<p>A common pattern I see is teams maintaining a central architecture repository that spans multiple services and platforms.
It’s an approach I use as well—but there’s a nuance.</p>
<h2>🏯 Architecture Has Layers</h2>
<p>Good architecture documentation spans multiple levels.</p>
<p>At a high level, you have:</p>
<p><strong>Business Architecture</strong> 👨‍💼</p>
<p>Capabilities, domains, and high-level concepts.</p>
<p><strong>Solution Architecture</strong> 👩‍🎨</p>
<p>Mapping capabilities to systems.</p>
<p>Defining which systems own what.</p>
<p>What gets replaced.</p>
<p>What gets invested in.</p>
<p><strong>System Architecture</strong> 👷</p>
<p>Services, databases, jobs, infrastructure, and how they interact.</p>
<p>These three layers belong in a central architecture repository.</p>
<p>But there is one more layer that many teams forget.</p>
<h2>🔍 The Missing Layer</h2>
<p><strong>Component Architecture</strong> 🧱</p>
<p>This is where architecture stops being abstract and starts becoming actionable.</p>
<p>Things like:</p>
<ul>
<li>Why middleware X was chosen over Y</li>
<li>How a request flows through a handler</li>
<li>What interfaces exist and why</li>
<li>What metrics and tracing are required</li>
<li>How a feature is expected to behave</li>
</ul>
<p>These decisions are too detailed for a central architecture repo—and too important to leave undocumented.</p>
<h2>📦 Put It in the Codebase</h2>
<p>Component-level architecture belongs with the code.</p>
<p>Not in a separate repo, not in a wiki, and certainly not only in tribal knowledge.</p>
<p>Why does this matter?
Because this is where architecture stops being abstract and becomes a specification.</p>
<p>A specification for engineers, reviewers, and now, coding agents.</p>
<h2>🤖 Why This Matters for Agents</h2>
<p>Agents don’t understand your system by osmosis the way human engineers do.</p>
<p>They only know what you give them.
If your architecture decisions live far away from your code, your agent:</p>
<ul>
<li>Misses constraints: Uses technologies that aren’t allowed in your environment</li>
<li>Guesses at design: Assumes <code>REST</code> when you expect event-driven messaging</li>
<li>Makes inconsistent choices: Pulls in the wrong libraries or dependencies</li>
</ul>
<p>But when that context lives in the repo, architecture:</p>
<ul>
<li>Evolves with the code</li>
<li>Is versioned</li>
<li>Is available at the moment it’s needed</li>
</ul>
<h2>🧠 Final Thought</h2>
<p>I’ve always believed the best engineers don’t just understand one codebase; they learn the end-to-end system.</p>
<p>The same applies to agents.</p>
<p>If you want better output from an agent, give it the same level of detail you’d give a senior engineer.</p>
]]></description>
        <pubDate>Thu, 04 Jun 2026 24:00:00 GMT</pubDate>
        
          <media:content url="https://bencane.com/assets/images/posts/2026-06-04-component-architecture.png" medium="image" />
        
      </item>
    
      
      
      
      
        
      
      <item>
        <title>Your coding agent is missing one thing: architectural context</title>
        <link>https://bencane.com/posts/2026-05-28-coding-agent-architectural-context/</link>
        
        <guid isPermaLink="false">https://bencane.com/posts/2026-05-28/</guid>
        <dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Benjamin Cane</dc:creator>
        <description><![CDATA[<p>Your coding agent is missing one thing: architectural context.</p>
<p>I’ve been a big believer in Architecture Decision Records (<code>ADRs</code>) long before coding agents came along.</p>
<p>Documenting decisions gives engineers context:</p>
<p>Why is the system designed a certain way?
What constraints existed at the time?
What tradeoffs were made?</p>
<p>That context matters.
It also matters for agents.</p>
<h2>🤖 Agents Need Context Too</h2>
<p>Unlike human engineers, agents don’t get context from hallway conversations, shadowing others, or tribal knowledge.</p>
<p>They only know what you capture.
The best way to capture architectural context?
Write it down as a decision record—and make it accessible to agents.</p>
<p>The only question is, what’s the best way to make decision records accessible?</p>
<h2>🏗️ Option 1: MCP Server</h2>
<p>If your <code>ADRs</code> live in a wiki or documentation system, you can expose them through an <code>MCP</code> server.</p>
<p>This works well when documentation is spread across teams or multiple systems that need to be aggregated.</p>
<p>You want a unified interface for agents.
<code>MCP</code> is a good approach, but it comes with some infrastructure overhead.</p>
<h2>🧱 Option 2: Keep ADRs in Git</h2>
<p>I’ve long preferred storing <code>ADRs</code> in Git.</p>
<p>It provides versioning, review workflows, automated validation, and is where engineering work happens.
Storing <code>ADRs</code> in Git, ideally alongside your code, is the fastest way to give agents usable context.</p>
<p>The challenge is that architecture often spans multiple services and repositories.
So many centralize their architecture into a single repository, which is not where your code lives.</p>
<h2>🌉 Bridging the Gap</h2>
<p>Most modern coding agents let you include additional directories or sources at runtime, either through slash commands or CLI options.</p>
<p>That means you can: open your codebase, include your architecture repository, and run the agent with context.</p>
<p>Just adding another directory gives your agent an understanding of system constraints, architecture decisions, technology choices, and surrounding systems.
These are not things an agent can reliably infer from code alone.</p>
<h2>💡 Final Thought: Why Context Matters</h2>
<p>With architectural context, agents produce code that aligns with your system.</p>
<p>When engineers understand the system end to end, they make better decisions.
The same applies to agents.</p>
<p>If you want better results, give better context.</p>
]]></description>
        <pubDate>Thu, 28 May 2026 24:00:00 GMT</pubDate>
        
          <media:content url="https://bencane.com/assets/images/posts/2026-05-28-architectural-context.png" medium="image" />
        
      </item>
    
      
      
      
      
        
      
      <item>
        <title>Health-check the listener your gRPC traffic actually uses</title>
        <link>https://bencane.com/posts/2026-05-21-grpc-health-check-listener/</link>
        
        <guid isPermaLink="false">https://bencane.com/posts/2026-05-21/</guid>
        <dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Benjamin Cane</dc:creator>
        <description><![CDATA[<p>One of the easiest ways to break a <code>gRPC</code> service in production is health-checking the wrong listener.</p>
<p>A common issue I see teams run into when adopting <code>gRPC</code> is leaving readiness checks pointed at their <code>HTTP</code> listener while production traffic actually flows through <code>gRPC</code>.</p>
<p>Everything looks fine until it suddenly doesn’t.</p>
<h2>🤔 The Problem</h2>
<p>Many <code>gRPC</code> services run two listeners: one for <code>HTTP</code> and one for <code>gRPC</code>.</p>
<p>The <code>HTTP</code> listener often exists for metrics, liveness checks, and management APIs.
Teams moving to <code>gRPC</code> often reuse the <code>HTTP</code> health checks they set up for their REST-based services.</p>
<p>It’s generally a good idea to reuse what you already have, but in this case, it can be misleading.</p>
<h2>⚠️ Health-Check What Serves Traffic</h2>
<p>If customers connect through <code>gRPC</code>, your first readiness check should too.</p>
<p>Your <code>HTTP</code> listener can be perfectly healthy while the <code>gRPC</code> listener is misconfigured, hung, or otherwise failing.</p>
<p>Meanwhile, Kubernetes, load balancers, and dashboards might all show green. ✅</p>
<p>This happens more often than people think.</p>
<h2>🩺 Better Ways to Monitor gRPC</h2>
<p>There are better ways to monitor your <code>gRPC</code> service.</p>
<h3>gRPC Health Probe ✅</h3>
<p>Use a real <code>gRPC</code> health check request against the listener.</p>
<p>This validates the actual serving path and confirms the service can respond over <code>gRPC</code>.</p>
<p>A strong default option.</p>
<h3>Build a Status gRPC Service 📋</h3>
<p>Expose an internal status method in your <code>gRPC</code> API.</p>
<p>This gives you flexibility to check deeper dependencies, such as database readiness, downstream systems, internal state, and maintenance toggles.</p>
<p>It’s more work, but more control.</p>
<h3>Use a Single Shared Listener ☝️</h3>
<p>Because <code>gRPC</code> runs on top of <code>HTTP/2</code>, many languages and frameworks can serve <code>HTTP</code> and <code>gRPC</code> traffic on the same listener.</p>
<p>That means an <code>HTTP</code> health endpoint may be acceptable because it checks the same network path.
It still does not fully validate <code>gRPC</code> behavior, but it is better than checking an entirely separate listener.</p>
<h2>🧠 Final Thoughts</h2>
<p><code>gRPC</code> is awesome.</p>
<p>But making a service production-ready means revisiting configurations inherited from REST services.</p>
<ul>
<li>Health checks</li>
<li>Load balancing behavior</li>
<li>Connection management</li>
<li>Contracts</li>
<li>Operational tooling</li>
</ul>
<p>None of these changes are difficult. They’re just easy to miss.</p>
]]></description>
        <pubDate>Thu, 21 May 2026 24:00:00 GMT</pubDate>
        
          <media:content url="https://bencane.com/assets/images/posts/2026-05-21-grpc-health-checks.png" medium="image" />
        
      </item>
    
      
      
      
      
        
      
      <item>
        <title>Weighted load balancing has saved me more times than I can count</title>
        <link>https://bencane.com/posts/2026-05-14-weighted-load-balancing/</link>
        
        <guid isPermaLink="false">https://bencane.com/posts/2026-05-14/</guid>
        <dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Benjamin Cane</dc:creator>
        <description><![CDATA[<p>Weighted load balancing has saved me more times than I can count.</p>
<p>Many engineers think of load balancers as simple traffic distributors.</p>
<p>Send requests across servers.
Keep systems available.
Move on.</p>
<p>But one of their most valuable capabilities is often overlooked.
Weighted load balancing.</p>
<h2>🤨 What Is Weighted Load Balancing?</h2>
<p>From enterprise hardware appliances to software load balancers like HAProxy and Envoy Proxy, nearly all modern load balancers support some form of weighted routing.</p>
<p>Start with a standard balancing algorithm, such as round-robin.
Then apply weights so some targets receive more traffic than others.</p>
<p>For example, if two targets are weighted at 90 and 10, roughly 90% of traffic goes to one target and 10% to the other.
If targets have equal weights, traffic is typically distributed evenly.</p>
<p>Simple idea, critical feature.</p>
<h2>🤔 Why It Matters</h2>
<p>Weighted load balancing turns migrations from risky big-bang cutovers into small, adjustable dials.</p>
<p>Instead of flipping traffic all at once, you can gradually shift production traffic while observing behavior in real time.</p>
<p>That means a smaller blast radius, easier rollbacks, and safer production migrations.</p>
<h2>🧰 What I Actually Use It For</h2>
<p>I’ve rarely used weighted load balancing because one server had more capacity than another.</p>
<p>What I’ve used it for repeatedly is change management.</p>
<p>Ten years ago, to migrate from a legacy file transfer platform to newer platforms.
We used weighted load balancing to introduce the new platform gradually.</p>
<p>Six years ago, to control which transactions were routed to our old card payments platform versus the new platform, we introduced weighted load balancing in our global transaction router.</p>
<p>Last night, to run a canary deployment, we adjusted our service mesh using weighted routing via <code>xDS</code>.</p>
<p>Different eras, different platforms, same core concept.</p>
<h2>🕰️ Standing the Test of Time</h2>
<p>Weighted load balancing is not new.
It has existed for a long time.</p>
<p>Foundational patterns often become the enablers for newer platform practices.
Canary releases, blue/green deployments, service mesh traffic shifting.</p>
<p>Many of those ideas rely on the same underlying capability: <strong>controlling traffic through percentages and weighting.</strong></p>
<p>Good patterns tend to survive generations of technology.</p>
<h2>🧠 Final Thoughts</h2>
<p>Many software engineers will never build a load balancer.
They will never configure one themselves.</p>
<p>But understanding what these systems can do is still an advantage.</p>
<p>Because migrations are often less about code and more about traffic control.</p>
<p>Writing good software isn’t enough.
Knowing when and how to shift 1% of traffic can make or break a migration.</p>
]]></description>
        <pubDate>Thu, 14 May 2026 24:00:00 GMT</pubDate>
        
          <media:content url="https://bencane.com/assets/images/posts/2026-05-14-weighted-load-balancing.png" medium="image" />
        
      </item>
    
      
      
      
      
        
      
      <item>
        <title>YOLO Is a Terrible Strategy for Validating Production Changes</title>
        <link>https://bencane.com/posts/2026-05-07-yolo-production-validation/</link>
        
        <guid isPermaLink="false">https://bencane.com/posts/2026-05-07/</guid>
        <dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Benjamin Cane</dc:creator>
        <description><![CDATA[<p>YOLO is a terrible strategy for validating production changes.</p>
<p>How many times have you seen it?</p>
<p>Your platform is running smoothly.
No alerts, no issues.
Then suddenly, something breaks.</p>
<p>After digging in, you discover the cause: another system you depend on made a change, and that change broke your platform.</p>
<p>They didn’t notice it broke. You did, much too late…</p>
<p>How many times have you been the cause of another platform breaking?</p>
<h2>🥶 Cold Reality</h2>
<p>I wish the above scenario were rare, but it happens constantly across the technology industry.</p>
<p>It happens between internal teams, third-party integrations, and shared infrastructure teams.</p>
<p>These scenarios make you wonder, “How was that change validated?”</p>
<p>Maybe they tested it, and their validation had gaps.
Maybe they did little validation at all.
If any.</p>
<p>Either way, the result is the same: <strong>they validated their change with 100% of production traffic.</strong>
Bad plan.</p>
<h2>💡 Better Ways to Validate Changes</h2>
<p>There are many ways teams can reduce production risk when rolling out changes, and the best teams combine the following approaches.</p>
<h3>Canary Releases 🐤</h3>
<p>I talk about canary deployments often.</p>
<p>Instead of moving 100% of traffic at once, move small percentages gradually and observe behavior closely.</p>
<p><strong>That observed part matters.</strong>
Look at error rates, latency changes (beyond normal platform warmup), resource spikes, and unexpected retries.
All of these indicate customer impact.</p>
<p>Canary deployments are one of the best ways to reduce the blast radius of changes, identify problems quickly, and self-correct.</p>
<h3>Shadow Traffic 🪞</h3>
<p>Traffic mirroring sends production traffic to a new version before routing live traffic there.</p>
<p>Responses are ignored, but you observe behavior and monitor the same signals you would with a canary release without sacrificing a customer request.</p>
<h3>Synthetic Traffic 🤖</h3>
<p>Synthetic traffic simulates user behavior continuously.
It’s great for monitoring customer experience, but also a great way to validate new deployments.</p>
<p>Route synthetic traffic to upgraded instances first and verify behavior before moving real traffic.
If it fails with synthetic traffic, it likely won’t survive real traffic.</p>
<h3>Smoke Tests 😶‍🌫️</h3>
<p>The classic approach.
After deployment, run a small set of fast tests to confirm the platform is fundamentally working.</p>
<p>Smoke tests don’t need to be fancy; they can be shell scripts, API calls, read-only requests, a test file, or full end-to-end validation.</p>
<p>Their purpose is simple: to quickly catch obvious breakage.</p>
<h2>🧠 Final Thoughts</h2>
<p>Don’t think of the above methods as mutually exclusive choices.
Combine them.</p>
<p>Some platforms I work on combine canary releases, shadow traffic, and synthetic traffic.
Others use smoke tests plus canary releases.</p>
<p>The more layers of validation you have, the more likely you are to catch issues before your customers do.
Because having your customers validate changes for you is a poor strategy.</p>
]]></description>
        <pubDate>Thu, 07 May 2026 24:00:00 GMT</pubDate>
        
          <media:content url="https://bencane.com/assets/images/posts/2026-05-07-production-validation.png" medium="image" />
        
      </item>
    
      
      
      
      
        
      
      <item>
        <title>Deterministic routing is one of the most effective ways distributed systems reduce consistency problems at scale</title>
        <link>https://bencane.com/posts/2026-04-30-deterministic-routing/</link>
        
        <guid isPermaLink="false">https://bencane.com/posts/2026-04-30/</guid>
        <dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Benjamin Cane</dc:creator>
        <description><![CDATA[<p>Deterministic routing is one of the most effective ways distributed systems reduce consistency problems at scale.</p>
<p>It is a foundational technique used by many modern databases, caches, and large-scale platforms.
Understand how it works and you can apply the same pattern in your own systems.</p>
<h2>🤔 Understanding the Problem</h2>
<p>At some point, every successful system hits the limits of a single database instance.</p>
<p>A single server can only handle so many connections, queries, writes, storage capacity, or CPU/memory demands.
Even with the best hardware, performance eventually degrades.
So systems scale horizontally.</p>
<p>Instead of sending all traffic to a single database server, requests are distributed across multiple nodes.</p>
<p>At the same time, resiliency matters.
If one server fails and all data resides there, the outage can be severe.</p>
<p>So modern databases spread data across multiple nodes, availability zones, and regions.</p>
<p>Distributing load and data solves both capacity and resiliency problems.
But it introduces another challenge.</p>
<p>How do you keep request behavior consistent when data is distributed across multiple systems?</p>
<h2>⚠️ Why Replication Is Not Enough</h2>
<p>Replication helps, but it does not solve every consistency problem.</p>
<p>Imagine a write lands on Server 1.
Immediately after, a read request for the same data lands on Server 67.
Will Server 67 have the latest version?
Maybe, but often not.</p>
<h3>Asynchronous Replication</h3>
<p>With asynchronous replication, Server 1 will accept the write and replicate the data to other servers in the background.
That means a follow-up read on any other node may return stale data.</p>
<h3>Synchronous Replication</h3>
<p>With synchronous replication, the write on Server 1 will wait for an acknowledgment from all replicas before returning a success.
While this improves consistency guarantees, it increases latency.</p>
<p>The farther apart a replica is, the worse this gets.
Local writes may be fast, but cross-region writes will be slow.
Plus, is it really feasible to replicate data across every single node?</p>
<p>So the question becomes: <em>How do you preserve consistency, without paying latency taxes?</em></p>
<h2>🔀 Route Requests to the Data</h2>
<p>A highly effective answer is deterministic routing.</p>
<p>Instead of moving data to where requests might land, move requests to where the data already exists.</p>
<p>If requests for the same key can go to the same node, you gain predictable ownership, reduced stale reads, lower coordination overhead, and easier horizontal scaling.</p>
<h2>👨‍🏫 How Deterministic Routing Works</h2>
<p>At a high level, the system needs a repeatable way to decide where requests should go.</p>
<p>A common approach is hashing.</p>
<ul>
<li>A hash of <code>user123</code> always goes to Node 7</li>
<li>A hash of <code>user456</code> always goes to Node 42</li>
</ul>
<p>As long as the same key produces the same result, requests can be consistently routed to the same owner.
Many modern databases implement deterministic routing through techniques like consistent hashing, partition maps, and shard ranges.</p>
<h2>🗺️ Where Routing Logic Lives</h2>
<p>Different systems solve routing in different places.</p>
<h3>Client-side Routing</h3>
<p>The client library knows the partition map and sends requests directly to the correct node.
Used by many distributed caches and databases.</p>
<h3>Proxy / Router Tier</h3>
<p>A small router sits in front of nodes and forwards traffic appropriately.
Useful when client behavior cannot be influenced.</p>
<h3>Server-side Forwarding</h3>
<p>Requests land anywhere, and the receiving node forwards internally to the owning node.
Simple for clients, doesn’t introduce a proxy failure point, but introduces complex cluster discovery/health monitoring.</p>
<p>Each model has tradeoffs.</p>
<h2>🧰 Routing Does Not Replace Replication</h2>
<p>Deterministic routing is powerful, but not magic.
What happens when the owning node is down?
You still need replication.</p>
<p>Modern databases combine both: deterministic routing for performance and ownership, plus replication for durability and failover.</p>
<h2>🧠 Why This Matters Beyond Databases</h2>
<p>Distributed databases use this approach, but it is not unique to them.</p>
<p>Deterministic routing can be used to solve: session ownership, user affinity, in-memory workflow coordination, work queue partitioning, and more.</p>
<p>I’ve used deterministic routing many times to solve load distribution and consistency problems.</p>
<p>At scale, the answer is not always more/better hardware.
Consistency and availability problems are not always solved with replication alone.</p>
<p>Sometimes the best answer is simply to send the request to the right place.</p>
]]></description>
        <pubDate>Thu, 30 Apr 2026 24:00:00 GMT</pubDate>
        
          <media:content url="https://bencane.com/assets/images/posts/2026-04-30-deterministic-routing.png" medium="image" />
        
      </item>
    
      
      
      
      
        
      
      <item>
        <title>When you think of microservices, you probably think of centralized shared services. But there&#39;s another valid pattern that is rarely discussed</title>
        <link>https://bencane.com/posts/2026-04-23-microservices-local-platform-pattern/</link>
        
        <guid isPermaLink="false">https://bencane.com/posts/2026-04-23/</guid>
        <dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Benjamin Cane</dc:creator>
        <description><![CDATA[<p>When you think of microservices, you probably think of centralized shared services.
But there’s another valid pattern that is rarely discussed: running the same microservice inside multiple platforms.</p>
<h2>🧩 How It Usually Works</h2>
<p>Most microservice designs follow the same model:</p>
<ul>
<li>Break systems into capabilities, teams, or functions</li>
<li>Deploy one shared service for each capability</li>
<li>Any platform that needs it calls that centralized service</li>
</ul>
<p>That works well for many cases, but it’s not the only model.</p>
<h2>🏗️ How We Got Here</h2>
<p>Before microservices, many organizations used Service-Oriented Architecture (SOA).</p>
<p>Despite being labeled as antiquated, SOA and microservices are not that different.
Both break down systems into capabilities that communicate with each other.
The biggest difference is scope.</p>
<p>In SOA, a “Payments Service” might own:</p>
<ul>
<li>Message parsing</li>
<li>Validation</li>
<li>Balance checks</li>
<li>Currency conversion</li>
<li>Settlement logic</li>
</ul>
<p>While other SOA services would own “Users” or “Accounting”.
Today, that payment service would be considered an entire platform, with each of those capabilities implemented as microservices within that domain.</p>
<p>Microservices are often the same idea as SOA, just at a more granular level.</p>
<h2>🎯 Why Centralization Became the Default</h2>
<p>One reason microservices gained traction was the need to avoid duplication.
Capabilities were often rebuilt across multiple systems.
For example, Currency Conversion is needed in Payments, Accounting, and many other platforms.</p>
<p>Duplication is not just wasteful, it creates real problems: logic drift, coordination overhead, and inconsistent outcomes across systems.
Packaging that capability as a standalone service solved real problems: build once, reuse everywhere.</p>
<h2>⚠️ The Downside of Centralization</h2>
<p>In cell-based architectures, platforms are usually designed to be self-contained and failure-isolated.
That means a mission-critical platform depending on a centralized service shared by other platforms can become a design smell.</p>
<ul>
<li>Cross-cell dependencies</li>
<li>Added latency</li>
<li>Shared failure domains</li>
<li>Complex failover scenarios</li>
</ul>
<p>So teams, once again, solve these problems by rebuilding the same capability locally.</p>
<h2>🔁 Another Option</h2>
<p>Instead of rebuilding the capability each time, deploy the same microservice codebase inside multiple platforms.
If both Payments and Accounting need a currency conversion service, deploy the same service within each platform.</p>
<p>It’s the same codebase and capability, but with local ownership and resilience.
You get reuse without forced centralization.</p>
<h2>🧪 Caveats from Experience</h2>
<p>This pattern works when applied carefully.</p>
<h3>1️⃣ Strong Ownership</h3>
<p>A shared codebase still needs a clear owning team.
Others can contribute, but someone must own quality, roadmap, and releases.</p>
<h3>2️⃣ Pick the Right Capabilities</h3>
<p>Not everything is a great fit.
Something like currency conversion is well-scoped, relatively stateless, and doesn’t have unique business logic based on which platform is calling it.
It’s a strong example.</p>
<p>But other services that have unique logic for each platform domain or require consistency across different platforms are less of a fit.</p>
<h3>3️⃣ Operational Discipline</h3>
<p>Using the same codebase doesn’t automatically solve all problems; you can still run into drift across platforms if each is running a different version.
Changes in behavior still sometimes need coordination.</p>
<p>But with a single codebase, these issues are far easier to address.</p>
<h2>💭 Final Thoughts</h2>
<p>Microservices gave us reusable building blocks.
Sometimes the best use of a microservice is not one centralized deployment.
Sometimes it’s many local deployments of the same capability.</p>
<p>Just reuse the software while maintaining autonomy.</p>
]]></description>
        <pubDate>Thu, 23 Apr 2026 24:00:00 GMT</pubDate>
        
          <media:content url="https://bencane.com/assets/images/posts/2026-04-23-microservice-pattern.png" medium="image" />
        
      </item>
    
      
      
      
      
        
      
      <item>
        <title>Are you using traffic mirroring in production? If not, try it out.</title>
        <link>https://bencane.com/posts/2026-04-16-traffic-mirroring-in-production/</link>
        
        <guid isPermaLink="false">https://bencane.com/posts/2026-04-16/</guid>
        <dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Benjamin Cane</dc:creator>
        <description><![CDATA[<p>Are you using traffic mirroring in production?
If not, you might be missing one of the safest ways to test and observe production changes.</p>
<h2>🚦 What is Traffic Mirroring?</h2>
<p>Traffic mirroring in <code>Istio</code> or <code>Envoy Proxy</code> lets you send a copy of live traffic to a secondary target.</p>
<p>When enabled, traffic to <code>/service</code> routes to <code>cluster1</code> as normal, and a mirrored copy is sent to <code>cluster2</code>.</p>
<p><strong>The key:</strong> mirrored traffic is fire-and-forget.
Responses are ignored and never impact the primary request.</p>
<h2>🧪 Why It’s Powerful</h2>
<h3>1️⃣ Shadow Traffic for Safe Testing</h3>
<p>The most common use case is shadow traffic.</p>
<p>When migrating platforms or deploying a new version of an application, you can send real traffic to the new system, observe behavior, and validate responses.</p>
<p>All without impacting users. No risky cutovers.
You see exactly how the new system behaves under real load.</p>
<h3>2️⃣ Out-of-Band Traffic Inspection</h3>
<p>Another powerful use case is traffic inspection.</p>
<p>Inline inspection is risky.
It adds latency, introduces new failure points, and becomes part of the critical path.</p>
<p>With traffic mirroring, you can inspect traffic, analyze requests, and detect anomalies.</p>
<p>All without impacting the primary path.</p>
<h2>😶‍🌫️ Reality Check</h2>
<p>It’s not perfect.
There is some overhead.</p>
<p>Mirroring adds load to the sidecar, which may or may not be acceptable for your system.
In my experience, it’s negligible, but it’s something you should measure in your own environment before deploying to production.</p>
<h2>🧠 Final Thoughts</h2>
<p>Traffic mirroring is one of the safest ways to validate migrations, test new systems, and observe real production behavior.</p>
<p>The hard part isn’t mirroring traffic.
It’s running two production systems in parallel.
That’s the real cost, and the real tradeoff.</p>
<p>But if you can afford that cost, traffic mirroring is an incredibly powerful tool.</p>
<p>If you want to dig deeper:</p>
<ul>
<li><a href="https://istio.io/latest/docs/tasks/traffic-management/mirroring/">Istio traffic mirroring docs</a> explain the workflow.</li>
<li><a href="https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#config-route-v3-routeaction-requestmirrorpolicy">Envoy request mirror policy docs</a> cover the lower-level routing behavior.</li>
</ul>
]]></description>
        <pubDate>Thu, 16 Apr 2026 24:00:00 GMT</pubDate>
        
          <media:content url="https://bencane.com/assets/images/posts/2026-04-16-traffic-mirroring.png" medium="image" />
        
      </item>
    
  </channel>
</rss>
