← Back to list

Simplifying backward compatibility with session affinity

By routing each session back to the commit where it started, Plaid removed a layer of versioning and made shipping both safer and faster.

S Santichaivekin in Plaid Engineering · 2026-06-09 13:37 · 84 claps · 7.3 min read
#software-engineering #fintech #backend-development #software-development #engineering
Open on Medium ↗
Wiki topics: TLS · Design Tools & Workflow FIN · Fintech & Banking 🌐 · Web Development

Simplifying backward compatibility with session affinity

By routing each session back to the commit where it started, Plaid removed a layer of versioning and made shipping both safer and faster.

By Santi Santichaivekin

When users connect a financial account with Plaid Link, they move through a session that spans several minutes of multi-step requests and responses. This creates a subtle infrastructure problem: if we slowly roll out new code changes, user interactions will hop between old code and new code. The two versions of the code need to be interoperable. This is the problem of backward compatibility.

The challenge of backward compatibility

In most systems, backward compatibility is just about one system being able to operate with different versions of another system. Session-based systems add an additional layer where they also need to be compatible with their future self. A session can start on one version of the code and continue in an older or newer version during percentage rollouts.

Left: service that interacts with other services must be compatible with different versions of other services. — Right: Service that interacts with different versions of itself in the future must be compatible with itself.

Left: service that interacts with other services must be compatible with different versions of other services. — Right: Service that interacts with different versions of itself in the future must be compatible with itself.

For Plaid Link, the compatibility question is even trickier because we model interactions as paths through directed graphs. Developers have to think about backward compatibility through the lens of graph-state-transition changes and not just back and forth responses. In rare cases, we could break things, and we really wanted to eliminate this class of issues to make Link extremely reliable.

The solution space

There are a few approaches when it comes to solving backward compatibility:

1. Allow incompatibility and recover when sessions break

The simplest option is to accept that some in-flight sessions may break during code rollout. This can be viable when sessions are short, incompatible changes are rare, or users can easily retry. For Link, none of these apply, and users often do not retry when they fail to connect their bank accounts.

Additionally, this approach also creates operational toil: if errors spike during or after a deployment, teams have to investigate whether the issue is a persistent regression or is caused by the backward compatibility during the rollout itself.

This approach is simple and is commonly used, but it does not work for Link.

2. Prevent incompatible changes with validation and tests

Another option is to enforce compatibility by validating and testing that all changes are compatible.

Schema-level validation can catch obvious contract breaks, such as removing a required field or changing a response shape. However, incompatibility can occur through how the code semantically interprets and uses the schema, even without structural changes.

Code-level validation goes further by exercising old and new versions together in integration tests. This is expensive but will consistently catch behavioral drift. AI code review tools are also helpful in catching incompatibility in code. Still, the test matrix grows with every supported version, and product development will require compatibility overhead.

Overall, validation is useful, but it does not eliminate the problem. We still need to ask product developers to reason about compatibility as part of feature work, which slows down development.

3. Introduce schema versioning

In addition to validation and testing, we can introduce explicit versioning. Versioning acts like different swim lanes for backward compatibility. This is a common pattern in the industry, and works especially well when multiple parties need to align and negotiate on a shared standard, such as the TLS protocol version.

This was the solution that we initially adopted when we developed Plaid Link. We started versioning Link graph schemas when we first introduced the Link graphs in 2020. We shipped a few schema versions in each production binary, and production pods negotiated the version all can support.

Still, while versioning makes incompatibility more explicit and easier to reason with, it does not remove the developer burden. Having to create versions and maintain different graph versions confuses product developers and decreases development velocity.

4. Use session affinity

The last option that we ended up choosing for this project was to move compatibility handling into the infrastructure layer. Instead of requiring application code to support multiple workflow versions, we changed network routing so each session is routed to a compatible execution environment. This changed the interface for product developers. With versioning, product developers still have to understand and maintain multiple versions. With session affinity, the problem moves to the infrastructure layer. The code contains exactly one schema version, and infrastructure makes sure in-flight sessions keep going to the right place.

On the left, we have versioning where application code implements features against multiple schemas. On the right, we have session affinity where request goes to the same commit.

On the left, we have versioning where application code implements features against multiple schemas. On the right, we have session affinity where request goes to the same commit.

There are many possible levels of affinity:

  • Connection affinity: route a session on the same long-lived connection, e.g. WebSocket.
  • Pod affinity: route a session back to the same Kubernetes pod, e.g. via Kubernetes Session Affinity.
  • Commit affinity: route a session back to any pod running the commit where the session started.
  • Version affinity: route a session back to a known compatible version, even though it might run different code.

Connection and pod affinity is often the preferred option because it provides the additional benefits of allowing systems to store session data and cache in the pods themselves, which often results in significant performance, latency, and cost improvements.

However, they couple product behavior to infrastructure lifecycle. Pods restart and scale up or down. We also have infrastructure migrations that rotate pods and nodes. When sessions depend on a pod staying alive, platform operations become hard to perform.

If we choose connection or pod affinity, we will need to implement user-facing recovery paths for cases where the underlying pod or node disappears mid-session. This would also complicate rollout monitoring: when developers roll out new code, it could be tricky to triage whether issues are coming from pods winding down or from the rollout itself.

If we rely on pod-based session affinity instead of commit-based, users will see more “Something went wrong.”

If we rely on pod-based session affinity instead of commit-based, users will see more “Something went wrong.”

Commit-level affinity provides the necessary properties without complicating infrastructure operations. A session does not need to go to the same pod to maintain compatibility. By routing each session back to the commit where it started instead of specific pods, pods are able to restart, rotate, and scale normally without killing sessions, while also preserving consistency.

Replacing versioning with session affinity

We chose session affinity with the goal of removing compatibility concerns from the Link development loop. Instead of requiring every workflow change to reason through in-flight rollout states, individual sessions see one coherent version of the system.

When a Link session starts, we associate it with a deployment group based on the current rollout traffic weight. Subsequent requests for that session are routed back to that same deployment group.

Conceptually, this provides commit-level consistency: a session continues on the version of the system it started on. In practice, sessions are routed to compatible deployment groups rather than individual commits. By the time deployment advances, sessions from the previous state have mostly drained.

Routing metadata is captured at session start and propagated through subsequent requests, allowing infrastructure to maintain session consistency throughout the session lifecycle.

After we completed session affinity, we were able to remove all incompatibility validations and versioning code:

  • We no longer need to split backward incompatible changes into multiple backward compatible deployments.
  • A Link graph change can ship as one coherent PR, both schema and code together.
  • We deleted a big part of our codebase: all versioning code and artifacts.
  • We removed all Link backward-compatibility tests, reducing the company’s total test compute by 30%.
  • Graph changes reach production 6 times faster after we remove version-negotiation systems.

Supporting rollbacks

Designing rollback behavior was the hardest part of this project.

When a deployment is aborted, in-flight sessions have two choices. They can stay on the version they started on, or they can move back to the previous stable version.

Both choices have tradeoffs. Staying preserves session consistency, but keeps some users on code we no longer trust. Moving back gets users onto safer code, but can switch a session to a different workflow version mid-flight and create the same compatibility issue we were trying to avoid.

At first, we treated this as a user-impact question: how many sessions would we expect to break in each case over a year? Because Link sessions are short and rollbacks are rare, the expected impact was under our error budget and was acceptable in either case.

Eventually, we came to realize that the bigger concern was operator behavior. Rollback should be the safest action during an incident. We did not want engineers to hesitate because a rollback might create a second wave of compatibility issues.

In the end, we chose the rule that made rollback predictable, bounded, and safe under pressure. Session affinity removes compatibility concerns from normal development. The rollback design makes sure the issues do not come back during incidents.

Supporting A/B testing

A/B testing is a cornerstone of Link development. Teams use experiments to validate product changes and understand how users move through Link.

Before removing versioning, developers had two ways to run experiments. They could put one code path in one workflow version, another code path in another version, and control the release percentage between them. Or they could keep one graph and one schema, then branch inside the workflow using feature flags.

After the migration, we remove the first model. There is one deployed graph, and experiment assignment routes users into different branches of the graph.

This did not reduce what we could test. Anything we could model as two graph versions can also be modeled as one graph with two branching paths. In practice, this is often easier to reason about. Monitoring, conversion analysis, and debugging all happen inside one workflow instead of across multiple versions.

Takeaway

The goal of this project was simple: to make a category of compatibility problems disappear from the Link development loop.

Developers no longer need to think through every old-new version pairing during a rollout. Each active session stays in the deployment lane it started in, and new code can move forward without old sessions needing to understand it. As a result, we have fewer versioning processes, fewer backward-compatibility tests, fewer PRs per graph edits, and one fewer thing for Link developers to worry about.

Session affinity is often also called sticky sessions because each user session “sticks” to a compatible deployment. To celebrate the project, we shipped stickers, sticky notes, tape, and super glue to the stickiest contributors across Plaid offices and remote teams.

If problems like this sound interesting, we’re hiring.


메타데이터
post_id
fa2da4d0d4b6
slug
simplifying-backward-compatibility-with-session-affinity-fa2da4d0d4b6
url
https://engineering.plaid.com/simplifying-backward-compatibility-with-session-affinity-fa2da4d0d4b6
canonical_url
https://engineering.plaid.com/simplifying-backward-compatibility-with-session-affinity-fa2da4d0d4b6
author_url
https://medium.com/@s.santichaivekin
status
ok
fetched_at
2026-06-14 11:28:49