Custom Conflict Resolution Policies

Earn 25 points (50 with Pro) in two steps

  1. ① Read through the lesson — each section gets a ✓ as you scroll through it.
  2. ② When every section has a ✓, tap Complete lesson.

0 of 11 read · keep scrolling

✦ See fewer ads and earn double points — 50 a lesson instead of 25 — with Pro

Designing and Implementing Custom Conflict Resolution Policies in Multi-Region Architectures

Introduction: The Challenge of Distributed State

In a globalized digital economy, applications are expected to be available 24/7 with minimal latency for users regardless of their geographic location. To achieve this, engineers frequently deploy database clusters across multiple regions. This architectural pattern allows a user in Tokyo to read from a local data center while a user in New York interacts with a local node on the East Coast. However, this convenience introduces the "CAP theorem" trade-off: when you prioritize availability and partition tolerance, you must eventually deal with the reality that data can be modified simultaneously in two different parts of the world.

When two users update the same record in two different regions at nearly the same time, the system faces a conflict. If the database simply overwrites one change with the other, data loss occurs. If it locks the entire system to ensure a strict sequence of events, latency spikes and the benefits of multi-region deployment vanish. This is where custom conflict resolution policies become essential. A conflict resolution policy is the logic that determines the final state of a record when concurrent updates occur. Understanding how to design, implement, and test these policies is a fundamental skill for any engineer working on large-scale distributed systems.


Not read yet

Understanding the Nature of Data Conflicts

Before diving into custom implementation, we must understand why conflicts happen. In a multi-region setup, data is typically replicated asynchronously to ensure that local writes remain fast. If Region A receives a write and Region B receives a different write for the same row before they have synchronized, the database enters an inconsistent state.

Categories of Conflicts

Conflicts generally fall into three categories that dictate how you might approach resolution:

  • Write-Write Conflicts: Two users attempt to update the same field in the same document. For example, a user updates their profile bio in London while simultaneously updating it in Singapore.
  • Delete-Write Conflicts: One user deletes a record while another user attempts to update it. The system must decide if the update should be ignored or if it should resurrect the record.
  • Add-Add Conflicts: Two users create the same unique entity (e.g., two users trying to claim the same username). This requires semantic conflict resolution rather than simple timestamp comparison.

Callout: The "Last Write Wins" Fallacy Many developers default to "Last Write Wins" (LWW) because it is simple to implement. LWW uses wall-clock timestamps to determine the winner. However, wall-clock time is notoriously unreliable in distributed systems due to clock skew—the phenomenon where servers' internal clocks drift apart. Relying solely on LWW often leads to data loss where the "latest" update is actually the one that arrived at a server with a faster clock, not the one that happened later in human time.


Not read yet

Designing Custom Resolution Policies

When the default behavior of your database is insufficient, you must design a custom resolution policy. The goal is to move from a blind overwrite approach to a semantic or deterministic approach that preserves the intent of the user.

1. Vector Clocks and Versioning

Instead of relying on timestamps, you can use logical clocks, such as Vector Clocks or Version Vectors. These structures track the causality of events. Each node maintains a counter for every other node in the system. When a change occurs, the version vector is updated. This allows the system to determine if one update strictly follows another, or if they are concurrent.

2. Semantic Merging (CRDTs)

Conflict-free Replicated Data Types (CRDTs) are data structures designed to be merged automatically without conflicts. For instance, if you are building a counter, you can use a G-Counter (Grow-only Counter). Each region keeps its own count, and the final value is simply the sum of all regional counts. This eliminates the need for conflict resolution entirely because the merge operation is commutative, associative, and idempotent.

3. Application-Level Resolution

Sometimes the database cannot resolve the conflict because it lacks context. In these cases, you implement a "Conflict Handler" in your application code. When the database detects a conflict, it stores both versions of the record (or a "tombstone" for deletions) and flags the object as conflicted. The next time the application reads this object, it triggers a function to resolve the conflict based on business rules.


Not read yet

Implementation Strategies with Code Examples

Let's look at how to implement a custom resolution policy in a hypothetical environment using a "Versioned Merge" strategy.

Example: Resolving Profile Updates

Imagine a user profile with a bio and a status field. We want to ensure that if a user updates the bio in one region and the status in another, both updates are kept.

/**
 * Custom Conflict Resolver
 * @param {Object} localRecord - The record existing in the local database
 * @param {Object} remoteRecord - The record received from another region
 * @returns {Object} - The resolved record
 */
function resolveProfileConflict(localRecord, remoteRecord) {
    // Check if the records have different versions
    if (localRecord.version === remoteRecord.version) {
        return localRecord; // No change needed
    }

    // Merge logic: Combine fields if they were updated independently
    const resolved = {
        id: localRecord.id,
        version: Math.max(localRecord.version, remoteRecord.version) + 1,
        bio: localRecord.lastModifiedBio > remoteRecord.lastModifiedBio 
             ? localRecord.bio 
             : remoteRecord.bio,
        status: localRecord.lastModifiedStatus > remoteRecord.lastModifiedStatus 
                ? localRecord.status 
                : remoteRecord.status
    };

    return resolved;
}

Explanation of the Code

In this implementation, we move away from a single "record version" and instead track the last modified time for individual fields. By comparing lastModifiedBio and lastModifiedStatus independently, we allow the user to modify different parts of their profile simultaneously without losing data. The version increment ensures that the resulting record is treated as a new, authoritative state in the replication stream.

Tip: Use Deterministic Logic Always ensure your conflict resolution logic is deterministic. If two nodes receive the same two conflicting versions, they must arrive at the exact same result. If your resolution function relies on non-deterministic data (like Math.random() or current system time), your cluster will diverge, creating permanent inconsistencies.


Not read yet

Step-by-Step Implementation Workflow

Implementing custom resolution is a multi-stage process. Follow these steps to ensure safety:

  1. Define the Conflict Detection Window: Identify which datasets are prone to contention. You do not need complex resolution for every table. Focus on high-traffic, multi-user entities like shopping carts, user settings, or session data.
  2. Choose the Data Structure: Decide if you can model the data as a CRDT (like a set or a counter) or if you need to store metadata (like timestamps or vector clocks) alongside your data.
  3. Implement the Resolver: Write the resolution function. It should be a pure function: it takes two inputs and returns one deterministic output.
  4. Register the Resolver: Configure your database (e.g., Cassandra, DynamoDB, or Riak) to use your custom resolver. Most distributed databases have a hook for "Read Repair" or "Conflict Resolution" that triggers your code.
  5. Test for Convergence: Create a simulation script that generates random concurrent writes in two regions and verifies that both regions eventually arrive at the same record state.

Not read yet

Best Practices for Multi-Region Consistency

1. Minimize Conflict Scope

The best way to handle conflicts is to avoid them. You can use "data sharding" or "regional ownership" to ensure that a specific user's data is only ever written in one region. For example, a user's session could be pinned to the region closest to them. If the user travels, you initiate a "handoff" process to move the data ownership to a different region.

2. Use Logical Clocks

Avoid system wall-clock time whenever possible. Use Hybrid Logical Clocks (HLC) if you need to maintain a readable timestamp that still respects causality. HLCs combine the benefits of physical clocks (for ordering events) with logical clocks (for tracking causality).

3. Log Conflicts for Observability

Never resolve a conflict silently and move on without keeping a record. Log the occurrence of a conflict and the resolution chosen. This is vital for auditing and debugging. If your resolution logic is flawed, you will need the audit logs to revert the data to a clean state.

4. Idempotency is Mandatory

Your resolution logic must be idempotent. If the same merge operation is applied twice, it should not change the result after the first application. This is a requirement for any distributed system where network retries are common.

Warning: The "Tombstone" Problem Deletions are the most dangerous operations in a distributed system. If you delete a record in Region A, that deletion must be propagated as a "tombstone" (a marker saying the record is gone). If another region adds the record back, the systems might get confused about whether the record is active or deleted. Always ensure your tombstone logic includes a version or a timestamp that is higher than any previous update to that record.


Not read yet

Comparison of Resolution Strategies

Strategy Complexity Best For Risk
Last Write Wins (LWW) Low Non-critical data High risk of data loss
Vector Clocks Medium Causal consistency Increases object size
CRDTs High Counters, Sets, Maps Limited to specific data types
Application Logic High Complex business rules Requires careful testing

Common Pitfalls and How to Avoid Them

Pitfall 1: Ignoring Network Partitions

Many developers assume the network will always be healthy. When a network partition occurs, the nodes in Region A cannot talk to Region B. If you design a system that requires a "majority vote" to resolve conflicts, the minority side will stop accepting writes. This is a valid design choice, but you must be prepared for the downtime. If your priority is uptime, you must accept that you will have to resolve conflicts after the network heals.

Pitfall 2: Over-Engineering

Do not apply complex CRDTs to data that rarely changes. If a record is updated once a month, simple versioning or even a manual resolution process might be cheaper and more reliable than a sophisticated automated system. Use the simplest tool that meets your consistency requirements.

Pitfall 3: Failing to Test Edge Cases

Engineers often test the "happy path" where two updates arrive at the same time. They rarely test the "unhappy path" where an update arrives, then a delete, then a late-arriving update from a third region. Use "chaos engineering" tools to simulate delayed packets and reordered messages to ensure your resolution logic holds up under stress.


Not read yet

Deep Dive: The Role of Causality

Causality is the relationship between events where one event influences another. In a distributed system, if User A updates their address and then sends a notification about the update, the address change is the "cause" and the notification is the "effect."

If these events are replicated across regions out of order, the system might try to process the notification before the address change exists. This is why custom resolution policies often need to be "causality-aware." By using Vector Clocks, you can ensure that the system refuses to process an effect until the cause has been applied.

Implementing a Causality-Aware Check

function canProcessEvent(event, state) {
    // Check if the dependencies of this event are already in the state
    for (let dependency of event.dependencies) {
        if (!state.has(dependency)) {
            return false; // Dependency missing, buffer this event
        }
    }
    return true;
}

This simple check prevents the "effect before cause" problem. By buffering events that arrive out of order, you maintain the logical integrity of your data.


Not read yet

Integrating Resolution into Microservices

In a microservices architecture, conflict resolution often happens at the API Gateway or the Service layer rather than the database layer. When a service receives a write request, it can check the version of the record it holds. If the version is stale, the service can trigger a "reconciliation flow."

The Reconciliation Pattern

  1. Request Arrival: Service receives a write request with a version number.
  2. Version Comparison: Service queries the database for the current version.
  3. Conflict Detected: If the version in the request is lower than the database version, the service rejects the write or initiates a merge.
  4. Merge and Retry: The service fetches the latest data, applies the user's changes to the new data, and attempts the write again.

This approach is highly effective because the service layer has business context that the database lacks. For example, if a user tries to withdraw money from a bank account, the service layer can check if the balance is sufficient after merging the latest data, preventing an overdraft.


Not read yet

Scaling Conflict Resolution

As your system grows, the cost of conflict resolution can become a bottleneck. If you have thousands of conflicts per second, running complex resolution logic on every read will kill your performance.

Strategies for Scaling:

  • Asynchronous Resolution: Instead of resolving on read, resolve in the background. Allow the user to see the "best guess" or a "pending" state while the system reconciles the data in the background.
  • Batching: If you have many updates to the same record, batch them together before applying conflict resolution. This reduces the number of merge operations.
  • Offloading: Use dedicated worker processes to handle conflict resolution. Keep the primary request/response path clean and fast.

Summary of Key Takeaways

  1. Understand the Trade-off: Multi-region writes force a choice between strict consistency and high availability. Custom conflict resolution is the bridge that makes high availability usable.
  2. Avoid LWW When Possible: "Last Write Wins" is easy but dangerous. Prioritize causal tracking or semantic merging (CRDTs) to preserve user intent and prevent data loss.
  3. Ensure Determinism: Any logic used to resolve conflicts must be deterministic. Every node in your system must arrive at the same result given the same input, or your data will permanently diverge.
  4. Design for Causality: Use logical clocks or version vectors to track the order of events. This prevents issues like processing a deletion before an update, or a notification before the underlying data exists.
  5. Test the Unhappy Path: Use simulation tools to test how your resolution logic handles out-of-order delivery, dropped packets, and network partitions.
  6. Keep it Simple: Use the simplest possible resolution strategy that meets your business needs. Do not implement complex CRDTs if simple field-level versioning will suffice.
  7. Monitor and Audit: Log every conflict and its resolution. You cannot fix what you cannot measure, and you cannot debug what you cannot see.

By implementing these strategies, you move from a fragile system that loses data to a resilient architecture that handles the complexities of the global internet. The key is to treat conflict resolution not as an afterthought, but as a core component of your data model design. When you design your data structures with the assumption that conflicts will happen, you build systems that are significantly more robust and easier to maintain over the long term.


Not read yet

FAQ: Common Questions

Q: Can I use a central locking service to prevent all conflicts? A: You can, but it defeats the purpose of a multi-region architecture. A central lock requires a round-trip to a single region, which introduces the very latency you are trying to avoid. Use locks only for extremely rare operations where consistency is more important than speed.

Q: Are CRDTs better than application-level resolution? A: CRDTs are generally safer because they are mathematically proven to converge. However, they are limited in the types of data they can represent. Use CRDTs for simple structures like counters and sets, and use application-level resolution for complex business logic.

Q: What happens if the conflict resolution logic itself has a bug? A: This is why logging is critical. If you discover a bug in your resolution logic, you will need the logs to identify which records were resolved incorrectly. You can then write a "repair script" to re-process those specific records using the corrected logic. Always ensure your resolution function is versioned so you can track which logic was applied to which record.

Q: How do I handle conflicts in a database that doesn't support custom hooks? A: You can implement the "versioning" approach at the application layer. Store a version field in your database. Every update must include the version number you read. If the update fails because the version has changed, fetch the new version, merge, and retry. This is known as "optimistic concurrency control."

Q: Is "Always Ask the User" a valid conflict resolution policy? A: Yes, in some cases. For collaborative editing (like Google Docs), the system merges what it can and highlights the rest for the user to resolve. This is often the best approach for high-value data where the system cannot determine the "correct" outcome.

Not read yet

Each section gets a ✓ as you scroll through it. Tap the button to jump to the next one.