CRDT - dlaczego świat migruje w stronę danych, które same rozwiązują konflikty

Poniedziałek, 9:00. Dwóch handlowców edytuje ten sam cennik w firmowym systemie. Adam zmienia cenę produktu A w Warszawie. Beata zmienia cenę produktu B w Krakowie. Obaj klikają "Zapisz" w tym samym momencie. Jeden z nich dostaje komunikat: "Błąd: rekord został zmieniony przez innego użytkownika. Odśwież stronę i wprowadź zmiany ponownie."

Beata odświeża. Jej 15 minut pracy zniknęło. Wpisuje zmiany od nowa. W międzyczasie Adam zmienił kolejną cenę. Beata znowu dostaje błąd. Dzwoni do IT. IT mówi: "Nie edytujcie tego samego cennika jednocześnie."

W 2026 roku "nie edytujcie jednocześnie" to nie rozwiązanie. To kapitulacja.

Istnieje matematycznie udowodniony sposób, żeby Adam i Beata mogli edytować ten sam cennik jednocześnie, offline, z różnych kontynentów - i żeby system sam połączył ich zmiany bez żadnego konfliktu. Nazywa się CRDT.

Czym jest CRDT

CRDT - Conflict-free Replicated Data Type - to struktura danych zaprojektowana tak, żeby mogła istnieć w wielu kopiach jednocześnie, być modyfikowana niezależnie na każdej kopii, bez koordynacji między nimi, i zawsze zbiegać do tego samego stanu.

Nie "zwykle". Nie "w większości przypadków". Zawsze. To gwarancja matematyczna, nie obietnica marketingowa.

Jak to działa - intuicja

Wyobraź sobie licznik odwiedzin strony. Masz 3 serwery. Każdy liczy odwiedziny lokalnie:

  • Serwer A: 150 odwiedzin
  • Serwer B: 230 odwiedzin
  • Serwer C: 90 odwiedzin

Gdy serwery się zsynchronizują, wynik to 150 + 230 + 90 = 470. Nie ma konfliktu. Nie ma "który serwer ma rację". Każdy dodał swoje, suma jest jednoznaczna. Kolejność synchronizacji nie ma znaczenia - A zsynchronizuje się z B, potem z C, albo C z A, potem z B. Wynik jest ten sam: 470.

To jest G-Counter - najprostszy CRDT. Grow-only counter. Ale ta sama zasada skaluje się do map, zbiorów, rejestrów, a nawet dokumentów tekstowych.

Dwie rodziny CRDT

RodzinaJak działaKiedy stosować
State-based (CvRDT)Repliki wymieniają pełny stan (lub deltę). Merge jest komutacyjny, asocjacyjny i idempotentny - kolejność i liczba merge'ów nie ma znaczeniaProste sieci, tolerancja na większe payloady
Operation-based (CmRDT)Repliki wymieniają operacje (np. "dodaj element X"). Operacje muszą być komutatywneGdy payloady muszą być małe, sieć gwarantuje dostarczenie

W praktyce większość implementacji w Elixirze używa delta-state CRDT - hybrydy, która wysyła tylko zmiany (delty) zamiast pełnego stanu, łącząc zalety obu podejść.

Najważniejsze typy CRDT

TypOpisZastosowanie
G-CounterLicznik rosnący (tylko inkrementacja)Zliczanie odwiedzin, eventów, kliknięć
PN-CounterDwa G-Countery (wzrost + spadek)Stany magazynowe, pule zasobów, salda
LWW-RegisterRejestr "wygrywa ostatni zapis"Pola profilu użytkownika, ustawienia konfiguracji
OR-SetZbiór z operacjami dodaj/usuń i śledzeniem przyczynowościTagi, listy członków, koszyk zakupowy
AWLWWMapMapa klucz-wartość z semantyką add-winsRozproszone słowniki, rejestry konfiguracji

Dlaczego świat migruje w stronę CRDT

Cztery siły napędzają tę zmianę jednocześnie.

1. Ruch local-first

Ink & Switch, laboratorium badawcze stojące za jednym z najważniejszych esejów ostatniej dekady ("Local-first software"), sformułowało prostą tezę: użytkownik powinien być właścicielem swoich danych, aplikacja powinna działać offline, a współpraca powinna odbywać się peer-to-peer.

CRDT to fundament technologiczny tego podejścia. Bez CRDT local-first jest życzeniem. Z CRDT - jest architekturą.

Społeczność local-first w 2026 roku jest porównywana do Reacta w 2013. Wcześnie, ale kierunek jest oczywisty.

2. Oczekiwanie współpracy w czasie rzeczywistym

Google Docs przyzwyczaiło ludzi do jednoczesnej edycji. Teraz każda aplikacja, która blokuje jednoczesny dostęp, wygląda jak relikt. Problem: Google Docs używa Operational Transform (OT), która wymaga centralnego serwera. CRDT osiąga ten sam efekt bez centralnego koordynatora - co oznacza, że działa także offline i peer-to-peer.

3. Edge computing i offline-first

Połowa firm wdrażających local-first potrzebuje offline'u dla pracowników w terenie - handlowców, serwisantów, kontrolerów jakości. Druga połowa chce szybszego UI - operacje lokalne z natychmiastową odpowiedzią, synchronizacja w tle.

CRDT obsługuje oba scenariusze, bo stan można modyfikować lokalnie i mergować później - bez ryzyka utraty danych, bez ręcznego rozwiązywania konfliktów.

4. Systemy rozproszone jako domyślna architektura

Aplikacje działają w wielu data center, regionach, na wielu urządzeniach. Konsensus rozproszony (Paxos, Raft) na każdym zapisie to latencja i złożoność. CRDT eliminuje potrzebę konsensusu - każda replika zapisuje lokalnie, synchronizacja jest asynchroniczna, spójność jest gwarantowana matematycznie.

CRDT vs tradycyjne podejścia

CRDT vs Operational Transform (OT)

WymiarCRDTOperational Transform
ArchitekturaZdecentralizowana, peer-to-peerWymaga centralnego serwera
OfflineNatywny - edytuj lokalnie, merguj późniejSłaby - serwer potrzebny do transformacji
Gwarancja spójnościMatematyczna (strong eventual consistency)Algorytmiczna (funkcje transformacji)
ZłożonośćW projekcie typu danychW algorytmie transformacji
SkalowalnośćLiniowa - każdy nowy węzeł to niezależna replikaOgraniczona przez centralny serwer
DojrzałośćNowsza, rosnący ekosystemDekady produkcyjnego użycia (Google Docs, Etherpad)

Kluczowy kompromis: OT lepiej zachowuje intencję użytkownika (wynik wygląda tak, jak użytkownicy oczekują), ale wymaga centralnego serwera. CRDT gwarantuje zbieżność bez koordynacji, ale wynik nie zawsze idealnie odpowiada intencji. W praktyce, dla większości zastosowań biznesowych, CRDT jest wystarczające i znacznie prostsze operacyjnie.

CRDT vs Last-Write-Wins

Last-Write-Wins (LWW) to najprostszy CRDT - rejestr, w którym wygrywa zapis z najnowszym timestampem. Problem: współbieżne zapisy są tracone. Adam zmienił cenę, Beata zmieniła opis - wygrywa jedno, drugie znika.

Bogatsze typy CRDT (OR-Set, PN-Counter, AWLWWMap) zachowują wszystkie współbieżne operacje i mergują je semantycznie. Adam zmienił cenę, Beata zmieniła opis - obie zmiany są zachowane, bo dotyczą różnych pól.

CRDT vs konsensus rozproszony (Paxos/Raft)

WymiarCRDTKonsensus (Paxos/Raft)
DostępnośćZawsze (AP w CAP)Wymaga kworum (CP w CAP)
Latencja zapisuLokalna, asynchroniczna synchronizacjaSynchroniczna zgoda przed zapisem
Partycja sieciowaObie strony piszą dalejMniejszość nie może pisać
Typy operacjiOgraniczone do mergowalnych typówDowolne
ZastosowanieLiczniki, zbiory, presence, konfiguracjaElekcja lidera, replikacja logów, transakcje

CRDT i konsensus to nie konkurencja - to narzędzia do różnych problemów. CRDT tam, gdzie dostępność jest ważniejsza niż natychmiastowa spójność. Konsensus tam, gdzie potrzebujesz jednego źródła prawdy w danym momencie.

CRDT w Elixirze i Phoenixie

Elixir nie "wspiera" CRDT jako dodatkową bibliotekę. CRDT jest wbudowane w fundamenty ekosystemu. Phoenix.Presence - mechanizm śledzenia obecności użytkowników - to CRDT. Horde - rozproszony supervisor - to CRDT. Nawet konfiguracja klastra w wielu aplikacjach opiera się na CRDT.

Phoenix.Presence - CRDT, którego używasz nie wiedząc o tym

Phoenix.Presence to prawdopodobnie najszerzej używana implementacja CRDT w ekosystemie Elixira. Każda aplikacja Phoenix z funkcjonalnością "kto jest online" używa CRDT pod spodem.

Architektura:

  • Zero single point of failure
  • Zero centralnego źródła prawdy
  • Zero zewnętrznych zależności (bez Redisa, bez bazy danych)
  • Samonaprawianie przez zbieżność CRDT

Pod spodem Phoenix.Presence używa Phoenix.Tracker, który łączy protokół heartbeat/gossip z delta-state CRDT. Każdy węzeł klastra prowadzi własną replikę stanu obecności i rozgłasza delty w regularnych interwałach.

defmodule MyApp.Presence do
  use Phoenix.Presence,
    otp_app: :my_app,
    pubsub_server: MyApp.PubSub
end

To jest cała konfiguracja. Od tego momentu masz rozproszony, samonaprawiający się system śledzenia obecności bez żadnej zewnętrznej infrastruktury.

Śledzenie obecności w LiveView:

def mount(%{"doc_id" => doc_id}, _session, socket) do
  topic = "doc:#{doc_id}"

  if connected?(socket) do
    MyApp.Presence.track(self(), topic, socket.assigns.current_user.id, %{
      name: socket.assigns.current_user.name,
      cursor: %{x: 0, y: 0},
      joined_at: DateTime.utc_now()
    })

    Phoenix.PubSub.subscribe(MyApp.PubSub, topic)
  end

  presences = MyApp.Presence.list(topic)
  {:ok, assign(socket, presences: presences)}
end

def handle_info(%Phoenix.Socket.Broadcast{event: "presence_diff", payload: diff}, socket) do
  presences =
    socket.assigns.presences
    |> MyApp.Presence.sync_diff(diff)

  {:noreply, assign(socket, presences: presences)}
end

Trzy węzły Phoenix, 500 użytkowników, zero Redisa. Użytkownik dołącza na węźle A - węzły B i C wiedzą o tym w ciągu milisekund. Węzeł A pada - informacja o rozłączonych użytkownikach propaguje się przez CRDT. Zero koordynacji, zero lidera, zero single point of failure.

Parametry konfiguracyjne Trackera:

ParametrDomyślna wartośćCo robi
:broadcast_period1500msInterwał rozgłaszania delt
:max_silent_periods10Periody ciszy przed wykryciem awarii (~15s)
:down_period30sTymczasowe oznaczenie węzła jako offline
:permdown_period20 minPermanentne usunięcie z repliki
:pool_size1Liczba shardów trackera

DeltaCrdt - CRDT ogólnego przeznaczenia

delta_crdt to główna biblioteka CRDT w Elixirze, stworzona przez Dereka Kraana. Implementuje delta-state CRDT z drzewem Merkle'a do efektywnej synchronizacji - zamiast wysyłać pełny stan, wysyła tylko zmiany od ostatniej synchronizacji.

# mix.exs
def deps do
  [{:delta_crdt, "~> 0.6"}]
end

Podstawowe użycie - rozproszona mapa klucz-wartość:

# Uruchom dwa węzły CRDT (AWLWWMap = Add-Wins Last-Write-Wins Map)
{:ok, crdt1} = DeltaCrdt.start_link(DeltaCrdt.AWLWWMap, sync_interval: 50)
{:ok, crdt2} = DeltaCrdt.start_link(DeltaCrdt.AWLWWMap, sync_interval: 50)

# Ustaw dwukierunkową synchronizację
DeltaCrdt.set_neighbours(crdt1, [crdt2])
DeltaCrdt.set_neighbours(crdt2, [crdt1])

# Zapis na węźle 1
DeltaCrdt.put(crdt1, "cena_produkt_A", 199_90)

# Po sync_interval (~50ms) węzeł 2 ma dane
DeltaCrdt.get(crdt2, "cena_produkt_A")
# => 199_90

# Jednoczesny zapis na obu węzłach - różne klucze
DeltaCrdt.put(crdt1, "cena_produkt_A", 179_90)
DeltaCrdt.put(crdt2, "cena_produkt_B", 299_90)

# Po synchronizacji oba węzły mają obie zmiany
DeltaCrdt.to_map(crdt1)
# => %{"cena_produkt_A" => 179_90, "cena_produkt_B" => 299_90}
DeltaCrdt.to_map(crdt2)
# => %{"cena_produkt_A" => 179_90, "cena_produkt_B" => 299_90}

W drzewie supervisorów:

children = [
  {DeltaCrdt, [crdt: DeltaCrdt.AWLWWMap, name: MyApp.ConfigStore]}
]

Supervisor.start_link(children, strategy: :one_for_one)

# Teraz cała aplikacja ma dostęp do rozproszonego store'a
DeltaCrdt.put(MyApp.ConfigStore, :feature_flags, %{new_ui: true, beta_api: false})

Praktyczny wzorzec - rozproszona konfiguracja runtime:

defmodule MyApp.RuntimeConfig do
  @moduledoc """
  Konfiguracja runtime replikowana automatycznie na wszystkie węzły klastra.
  Zmiana na jednym węźle propaguje się do wszystkich w ciągu milisekund.
  """

  def child_spec(_opts) do
    %{
      id: __MODULE__,
      start: {__MODULE__, :start_link, []},
      type: :worker
    }
  end

  def start_link do
    {:ok, pid} = DeltaCrdt.start_link(DeltaCrdt.AWLWWMap,
      name: __MODULE__,
      sync_interval: 100,
      on_diffs: fn diffs ->
        # Reakcja na zmiany z innych węzłów
        for {key, _old, new} <- diffs do
          Phoenix.PubSub.broadcast(MyApp.PubSub, "config:#{key}", {:config_changed, key, new})
        end
      end
    )

    # Połącz z innymi węzłami w klastrze
    neighbours = Node.list() |> Enum.map(&{__MODULE__, &1})
    DeltaCrdt.set_neighbours(pid, neighbours)

    {:ok, pid}
  end

  def get(key), do: DeltaCrdt.get(__MODULE__, key)
  def put(key, value), do: DeltaCrdt.put(__MODULE__, key, value)
  def delete(key), do: DeltaCrdt.delete(__MODULE__, key)
  def all, do: DeltaCrdt.to_map(__MODULE__)
end

Zmiana feature flaga na jednym węźle - propagacja na cały klaster bez restartu, bez deploymentu, bez zewnętrznej bazy konfiguracji. CRDT gwarantuje, że wszystkie węzły będą miały identyczny stan.

Horde - rozproszony supervisor na CRDT

Horde to rozproszone wersje DynamicSupervisor i Registry z biblioteki standardowej Elixira, zbudowane na DeltaCrdt. Drop-in replacement - ten sam interfejs, rozproszone działanie.

defmodule MyApp.Application do
  use Application

  def start(_type, _args) do
    children = [
      {Horde.DynamicSupervisor,
       [name: MyApp.DistributedSupervisor, strategy: :one_for_one]},
      {Horde.Registry,
       [name: MyApp.DistributedRegistry, keys: :unique]}
    ]

    Supervisor.start_link(children, strategy: :one_for_one)
  end
end

Jeden proces na klucz w całym klastrze:

# Uruchom proces obsługujący klienta - gdziekolwiek w klastrze
Horde.DynamicSupervisor.start_child(
  MyApp.DistributedSupervisor,
  {MyApp.CustomerSession, customer_id: "cust_123"}
)

# Znajdź ten proces z dowolnego węzła
[{pid, _}] = Horde.Registry.lookup(MyApp.DistributedRegistry, "cust_123")
GenServer.call(pid, :get_state)

Co się dzieje, gdy węzeł pada:

  1. CRDT wykrywa brak heartbeatu z węzła
  2. Informacja propaguje się do wszystkich replik
  3. Horde automatycznie restartuje procesy z upadłego węzła na żywych węzłach
  4. Registry aktualizuje się - PID-y wskazują na nowe lokalizacje
  5. Aplikacja działa dalej bez interwencji operatora

To jest kluczowe: CRDT nie jest tu ciekawostką akademicką. Jest fundamentem, na którym działa automatyczna redystrybucja procesów w klastrze. Bez CRDT Horde potrzebowałby centralnego koordynatora (single point of failure) albo protokołu konsensusu (wolniejszy, bardziej złożony).

Rozwiązywanie konfliktów nazw w Registry:

Gdy partycja sieciowa powoduje, że dwa węzły rejestrują ten sam klucz, CRDT po reunifikacji wykrywa duplikat. Horde wybiera zwycięzcę i wysyła przegranej stronie sygnał wyjścia:

# W module procesu - obsługa przegranej w konflikcie nazwy
def handle_info({:name_conflict, {name, _value}, _registry, winning_pid}, state) do
  # Opcjonalnie: przenieś stan do zwycięskiego procesu
  GenServer.cast(winning_pid, {:merge_state, state})
  {:stop, :name_conflict, state}
end

Phoenix.Sync i ElectricSQL - CRDT między serwerem a klientem

Najnowszy gracz w ekosystemie. ElectricSQL to warstwa synchronizacji, która łączy PostgreSQL z przeglądarką lub aplikacją mobilną. Silnik synchronizacji napisany jest w Elixirze. Phoenix.Sync to oficjalna integracja z Phoenixem.

# mix.exs
def deps do
  [{:phoenix_sync, "~> 0.6"}]
end

Dwa tryby pracy:

TrybJak działaKiedy używać
EmbeddedElectric działa jako zależność aplikacji, streamuje dane przez wewnętrzne API ElixiraProsta architektura, jeden deployment
HTTPŁączy się z zewnętrznym serwisem Electric przez HTTPOsobne skalowanie sync engine'u

ElectricSQL czyta WAL (Write-Ahead Log) PostgreSQL i streamuje zmiany do klientów. Klienci pracują na lokalnej bazie (PGlite, SQLite). Zmiany lokalne propagują się z powrotem do PostgreSQL. Konflikty rozwiązywane automatycznie.

To domyka pętlę: CRDT nie tylko między węzłami BEAM w klastrze, ale między serwerem a urządzeniami końcowymi - przeglądarkami, telefonami, tabletami. Handlowiec w terenie bez zasięgu edytuje dane lokalnie. Dane synchronizują się automatycznie po odzyskaniu połączenia. Zero konfliktów, zero utraconej pracy.

Dlaczego BEAM jest naturalnym domem dla CRDT

CRDT w Javie, Go czy Node.js to biblioteka, którą dodajesz do runtime'u, który nie był projektowany z myślą o dystrybucji. CRDT na BEAM to naturalne rozszerzenie maszyny wirtualnej, która od 1986 roku jest budowana do systemów rozproszonych.

Procesy BEAM to naturalne repliki CRDT

Procesy BEAM nie dzielą pamięci. Komunikują się przez wiadomości. Każdy proces może trzymać własną replikę CRDT, a mergowanie odbywa się przez passing wiadomości - bez locków, bez współdzielonego mutable state.

W Javie potrzebujesz ConcurrentHashMap, synchronized bloków, volatile pól. W Elixirze - wysyłasz wiadomość.

Transparentna dystrybucja

BEAM jest jedyną szeroko używaną maszyną wirtualną z wbudowanym modelem dystrybucji. Wysłanie wiadomości do procesu na innym serwerze wygląda identycznie jak wysłanie do procesu lokalnego:

# Lokalna wiadomość
send(local_pid, {:merge, delta})

# Zdalna wiadomość - ten sam kod
send(remote_pid, {:merge, delta})

DeltaCrdt na węźle A synchronizuje się z DeltaCrdt na węźle B dokładnie tym samym mechanizmem co z procesem lokalnym. Zero konfiguracji sieciowej, zero serializacji, zero dodatkowych warstw transportowych.

Garbage collector per-proces

GC na BEAM działa per-proces - sub-milisekundowe pauzy, izolowane do jednego procesu. To krytyczne dla CRDT, gdzie operacje merge'owania są częste i nieprzewidywalne. W Javie stop-the-world GC może wstrzymać całą synchronizację CRDT w klastrze. Na BEAM - merge w procesie A nie wpływa na merge w procesie B.

Preemptywny scheduler

Scheduler BEAM wymusza sprawiedliwy podział CPU (~4000 redukcji na timeslice). Duży merge CRDT nie zagłodzi innych procesów. W Node.js (single-threaded event loop) ciężki merge blokuje cały serwer. W BEAM - jest jednym z milionów procesów, każdy z gwarantowanym czasem CPU.

Let it crash + CRDT = self-healing

Proces CRDT pada? Supervisor restartuje go. Zrestartowany proces synchronizuje się z sąsiadami i odzyskuje pełny stan. To jest beauty CRDT na BEAM - odporność na awarie jest wbudowana w oba systemy jednocześnie:

  • BEAM: supervisor restartuje proces
  • CRDT: re-synchronizacja odbudowuje stan

Żaden inny runtime nie daje obu tych gwarancji out of the box.

Zastosowania w praktyce

ZastosowanieImplementacjaSzczegóły
Śledzenie obecnościPhoenix.PresenceKto jest online, pozycje kursorów, wskaźniki "pisze..." - bez Redisa, bez bazy
Rozproszone procesyHorde (DeltaCrdt)Jeden proces na klucz w całym klastrze - sesje użytkowników, urządzenia IoT, pokoje czatu
Konfiguracja runtimeDeltaCrdtFeature flagi, parametry systemu - zmiana na jednym węźle, propagacja na cały klaster
Rozproszony rate limitingDeltaCrdtToken bucket replikowany w klastrze - spójne limity bez centralnego koordynatora
Synchronizacja offlinePhoenix.Sync / ElectricSQLDane PostgreSQL na urządzeniu mobilnym, praca offline, automatyczna synchronizacja
Rozproszone cache'owanieDeltaCrdtIn-memory cache spójny w klastrze - bez Redisa, bez Memcached
Kolaboratywna edycjaCustom CRDT + Phoenix PubSubWielu użytkowników edytuje ten sam dokument jednocześnie

Wzorzec: kolaboratywna edycja w LiveView

defmodule MyAppWeb.DocumentLive do
  use MyAppWeb, :live_view

  def mount(%{"id" => doc_id}, _session, socket) do
    topic = "document:#{doc_id}"

    if connected?(socket) do
      Phoenix.PubSub.subscribe(MyApp.PubSub, topic)

      MyApp.Presence.track(self(), topic, socket.assigns.current_user.id, %{
        name: socket.assigns.current_user.name,
        cursor: %{line: 0, col: 0},
        color: random_color()
      })
    end

    doc = MyApp.Documents.get!(doc_id)

    {:ok, assign(socket,
      doc: doc,
      topic: topic,
      presences: MyApp.Presence.list(topic)
    )}
  end

  def handle_event("edit", %{"field" => field, "value" => value}, socket) do
    # Zapisz zmianę lokalnie
    {:ok, doc} = MyApp.Documents.update_field(socket.assigns.doc, field, value)

    # Broadcast operacji (nie stanu) - inne klienty mergują
    Phoenix.PubSub.broadcast(MyApp.PubSub, socket.assigns.topic, %{
      event: :field_updated,
      field: field,
      value: value,
      user_id: socket.assigns.current_user.id,
      timestamp: DateTime.utc_now()
    })

    {:noreply, assign(socket, doc: doc)}
  end

  def handle_event("cursor_move", %{"line" => line, "col" => col}, socket) do
    MyApp.Presence.update(self(), socket.assigns.topic,
      socket.assigns.current_user.id,
      fn meta -> %{meta | cursor: %{line: line, col: col}} end
    )

    {:noreply, socket}
  end

  def handle_info(%{event: :field_updated, field: field, value: value, user_id: uid}, socket) do
    if uid != socket.assigns.current_user.id do
      doc = MyApp.Documents.apply_remote_change(socket.assigns.doc, field, value)
      {:noreply, assign(socket, doc: doc)}
    else
      {:noreply, socket}
    end
  end

  def handle_info(%Phoenix.Socket.Broadcast{event: "presence_diff", payload: diff}, socket) do
    presences = MyApp.Presence.sync_diff(socket.assigns.presences, diff)
    {:noreply, assign(socket, presences: presences)}
  end
end

Phoenix.Presence (CRDT) śledzi, kto edytuje dokument i gdzie jest kursor. PubSub propaguje operacje edycji. Każdy klient aplikuje zmiany lokalnie. Wynik: Google Docs w Twojej aplikacji Phoenix bez żadnej zewnętrznej infrastruktury.

Kiedy CRDT nie jest odpowiedzią

Uczciwie - nie każdy problem wymaga CRDT.

Silna spójność jest wymagana - systemy transakcyjne, rezerwacje, aukcje. Gdy dwa bilety lotnicze nie mogą być sprzedane na to samo miejsce, potrzebujesz konsensusu, nie CRDT. CRDT gwarantuje eventual consistency - "w końcu będzie spójne". Dla biletu lotniczego "w końcu" jest za późno.

Dane są write-heavy z jednym źródłem - logi, telemetria, strumienie eventów. Gdy jeden producent generuje dane, a wielu konsumentów czyta, CRDT jest overkill. Append-only log wystarczy.

Merge nie ma sensu - nie każdy typ danych da się sensownie zmergować. Co znaczy "zmerguj dwa zamówienia złożone jednocześnie na ten sam produkt"? Połowa + połowa? Dwa osobne? To zależy od logiki biznesowej, nie od struktury danych.

Overhead pamięciowy jest problemem - CRDT przechowuje metadane do śledzenia przyczynowości (vector clocks, dot contexts). Dla milionów drobnych wartości ten overhead może być istotny. G-Counter na 100 węzłach to 100 integerów zamiast jednego.

Przyszłość: Mnesia z CRDT

Warto wspomnieć o inicjatywie, która może zmienić cały ekosystem. Erlang Solutions sponsoruje prace nad dodaniem natywnego rozwiązywania konfliktów opartego na CRDT do Mnesia - rozproszonej bazy danych wbudowanej w OTP.

Mnesia istnieje od lat 90. i jest używana w systemach telekomunikacyjnych na całym świecie. Jej główna słabość: rozwiązywanie konfliktów po partycji sieciowej wymaga ręcznej interwencji. CRDT eliminuje ten problem - konflikty rozwiązują się automatycznie, matematycznie poprawnie.

Jeśli CRDT trafi do Mnesia, każda aplikacja Elixirowa będzie miała dostęp do rozproszonej bazy danych z automatycznym rozwiązywaniem konfliktów bez żadnych zewnętrznych zależności. Zero Redisa, zero PostgreSQL dla danych tymczasowych, zero konfiguracji. Mnesia + CRDT + supervision trees = samoleczący się, rozproszony storage wbudowany w runtime.

Stack CRDT w Elixirze - podsumowanie

WarstwaNarzędzieTyp CRDTZakres
Obecność użytkownikówPhoenix.PresenceDelta-stateWbudowane w Phoenix
Rozproszone procesyHordeAWLWWMap (DeltaCrdt)Klaster BEAM
Storage klucz-wartośćDeltaCrdtAWLWWMap, inneKlaster BEAM
Synchronizacja klient-serwerPhoenix.Sync / ElectricSQLColumn-level mergePrzeglądarka ↔ PostgreSQL
Baza rozproszona (przyszłość)Mnesia + CRDTW trakcie pracOTP

Pięć warstw, jedna filozofia: dane, które same rozwiązują konflikty. Od śledzenia kursorów po synchronizację offline, od konfiguracji runtime po rozproszone procesy.

Elixir nie "obsługuje" CRDT. Elixir żyje CRDT. Phoenix.Presence to CRDT, którego używasz od pierwszego mix phx.new. Horde to CRDT, który zarządza Twoimi procesami. DeltaCrdt to CRDT, który replikuje Twoje dane. A BEAM to runtime, który sprawia, że to wszystko działa naturalnie - bo był projektowany do systemów rozproszonych zanim CRDT miało nazwę.

Budujesz system, w którym wielu użytkowników musi pracować na tych samych danych jednocześnie - online i offline? Porozmawiajmy - pokażemy, jak CRDT w Phoenix rozwiązuje problem, z którym walczysz.