WhatsApp Benzeri Bir Chat Uygulaması Nasıl Tasarlanır?

Bir chat uygulaması ilk bakışta “A kullanıcısı B kullanıcısına mesaj yollar” kadar basit görünür. Ama işin içine gerçek zamanlı iletişim, grup mesajlaşması, okundu bilgisi, son görülme ve ölçeklenebilirlik girince sistem tasarımı daha ilginç hale gelir. Bu yazıda WhatsApp benzeri bir uygulamanın temel mimarisini, servislerin görevlerini ve mesajların sistem içinde nasıl aktığını adım adım göreceksin.

Bir chat uygulaması ilk bakışta “A kullanıcısı B kullanıcısına mesaj yollar” kadar basit görünür. Ama işin içine gerçek zamanlı iletişim, grup mesajlaşması, okundu bilgisi, son görülme ve ölçeklenebilirlik girince sistem tasarımı daha ilginç hale gelir. Bu yazıda WhatsApp benzeri bir uygulamanın temel mimarisini, servislerin görevlerini ve mesajların sistem içinde nasıl aktığını adım adım göreceksin.

Temel Problem: Gerçek Zamanlı Mesajlaşma

Bir chat sisteminde en kritik beklenti mesajın hızlı ulaşmasıdır. Kullanıcı mesaj gönderdiğinde karşı tarafın uygulaması mümkün olduğunca anında haberdar olmalıdır. HTTP bu iş için tek başına çok uygun değildir. Çünkü HTTP’de istemci istek atar, sunucu cevap verir. Sunucunun istediği anda istemciye veri göndermesi doğal akışın parçası değildir.

Burada WebSocket devreye girer. Yani şöyle düşün: kullanıcı uygulamayı açtığında sunucuyla sürekli açık kalan bir bağlantı kurar. Böylece sunucu, yeni mesaj geldiğinde istemciye beklemeden veri gönderebilir.

flowchart LR
    A["Kullanıcı A"] --> G1["Gateway 1"]
    B["Kullanıcı B"] --> G2["Gateway 2"]

    G1 --> S["Session Service"]
    G2 --> S

    S --> M["Message Store"]
    S --> G2
    G2 --> B

Bu diyagramda kullanıcıların farklı gateway sunucularına bağlı olabileceğini görüyoruz. Session Service, hangi kullanıcının hangi gateway üzerinde aktif bağlantısı olduğunu bilir ve mesajı doğru yere yönlendirir.

Gateway Neden Akıllı Olmamalı?

Gateway, dış dünyadan gelen bağlantıların sisteme giriş noktasıdır. Ama gateway’in çok fazla iş yapması iyi bir fikir değildir. Çünkü WebSocket bağlantıları uzun süre açık kalır ve her bağlantı sunucuda bellek tüketir.

Bu yüzden gateway mümkün olduğunca sade kalmalıdır. Bunu şöyle hayal edebilirsin: gateway sadece kapıdaki görevli gibi davranır; gelen paketi alır ve içerideki doğru servise iletir. Paketin içeriğini yorumlamak, kullanıcı ilişkilerini çözmek veya grup üyelerini hesaplamak başka servislerin işidir.

flowchart TD
    C["Mobil Uygulama"] --> GW["Gateway"]
    GW --> P["Parser Service"]
    P --> SS["Session Service"]
    SS --> MS["Message Store"]
    SS --> GS["Group Service"]
    SS --> LS["Last Seen Service"]

    subgraph "Gateway Katmanı"
        GW
    end

    subgraph "İç Servisler"
        P
        SS
        MS
        GS
        LS
    end

Parser Service gelen ham mesajı sistemin anlayacağı iç formata dönüştürür. Session Service mesajın nereye gideceğini bulur. Group Service grup üyeliklerini yönetir. Last Seen Service ise kullanıcı aktivitesini takip eder.

Bire Bir Mesajlaşma Akışı

Bire bir mesajlaşmada temel akış şudur: gönderen kullanıcı mesajı gateway’e yollar, gateway bunu iç servislere aktarır, mesaj kalıcı olarak saklanır ve alıcı kullanıcı bağlıysa ona iletilir.

Buradaki önemli ayrım “sent”, “delivered” ve “read” durumlarıdır. Sent, sistem mesajı aldı demektir. Delivered, mesaj karşı cihaz tarafından alındı demektir. Read ise kullanıcı ilgili sohbeti açıp mesajı gördü demektir.

sequenceDiagram
    participant A as Kullanıcı A
    participant G1 as Gateway 1
    participant S as Session Service
    participant DB as Message Store
    participant G2 as Gateway 2
    participant B as Kullanıcı B

    A->>G1: Mesaj gönder
    G1->>S: Mesajı ilet
    S->>DB: Mesajı kaydet
    S-->>G1: Sent bilgisi
    G1-->>A: Tek tik

    S->>G2: Mesajı B bağlantısına yönlendir
    G2->>B: Mesajı gönder
    B-->>G2: Alındı onayı
    G2-->>S: Delivered bildirimi
    S-->>G1: A'ya teslim bilgisini yönlendir
    G1-->>A: Çift tik

Bu akışta mesaj önce veritabanına yazıldığı için sistem daha güvenli hale gelir. Alıcı o an çevrimdışıysa mesaj kaybolmaz; daha sonra tekrar gönderilebilir.

Okundu Bilgisi Nasıl Çalışır?

Okundu bilgisi, teslim bilgisinden farklıdır. Mesaj cihaza ulaşmış olabilir ama kullanıcı sohbet ekranını açmamış olabilir. Bu yüzden read receipt ayrı bir olay olarak ele alınır.

Yani şöyle düşün: uygulama mesajı arka planda aldıysa delivered oluşur. Kullanıcı konuşmayı açtığında ise read oluşur.

sequenceDiagram
    participant B as Kullanıcı B
    participant G2 as Gateway 2
    participant S as Session Service
    participant G1 as Gateway 1
    participant A as Kullanıcı A

    B->>G2: Sohbet ekranı açıldı
    G2->>S: Read bildirimi
    S->>G1: Gönderene okundu bilgisini ilet
    G1->>A: Okundu durumunu göster

Burada teslim ve okundu durumlarının ayrı tasarlanması önemlidir. Çünkü ikisi farklı kullanıcı davranışlarını temsil eder.

Son Görülme ve Çevrim İçi Durumu

Son görülme bilgisini hesaplamak için kullanıcının yaptığı aktiviteler takip edilir. Mesaj göndermek, sohbet açmak veya uygulama içinde anlamlı bir işlem yapmak kullanıcı aktivitesi sayılabilir.

Ama her istek aktivite değildir. Örneğin uygulamanın arka planda mesaj kontrol etmesi kullanıcının gerçekten çevrim içi olduğu anlamına gelmez. Bu yüzden istemciden gelen isteklerde “bu kullanıcı aksiyonu mu, sistem isteği mi?” ayrımı yapılmalıdır.

flowchart TD
    R["İstemciden istek gelir"] --> Q{"Kullanıcı aktivitesi mi?"}
    Q -- "Evet" --> L["Last Seen Service'e gönder"]
    L --> U["last_seen timestamp güncelle"]
    Q -- "Hayır" --> N["Son görülme güncellenmez"]

    B["Başka kullanıcı sorgular"] --> LS["Last Seen Service"]
    LS --> T{"Son aktivite eşik içinde mi?"}
    T -- "Evet" --> O["Çevrim içi göster"]
    T -- "Hayır" --> P["Son görülme zamanını göster"]

Bu diyagram son görülme bilgisinin nasıl üretildiğini gösteriyor. Kullanıcı çok yakın zamanda aktif olduysa sistem “çevrim içi” diyebilir; daha eski bir aktivite varsa timestamp gösterilir.

Grup Mesajlaşması

Grup mesajlaşmasında problem biraz büyür. Çünkü artık bir mesaj tek bir kullanıcıya değil, grubun tüm üyelerine ulaştırılmalıdır. Buna fan-out denir. Yani şöyle düşün: tek bir mesaj, birçok alıcı için ayrı ayrı teslim operasyonuna dönüşür.

Grup üyelik bilgisini Session Service içinde tutmak doğru değildir. Bu bilgi ayrı bir Group Service tarafından yönetilmelidir.

flowchart LR
    U["Gönderen Kullanıcı"] --> GW["Gateway"]
    GW --> S["Session Service"]
    S --> G["Group Service"]
    G --> S

    S --> DB["Message Store"]

    S --> R1["Alıcı 1 Gateway"]
    S --> R2["Alıcı 2 Gateway"]
    S --> R3["Alıcı 3 Gateway"]

    R1 --> A1["Grup Üyesi 1"]
    R2 --> A2["Grup Üyesi 2"]
    R3 --> A3["Grup Üyesi 3"]

Burada Session Service, Group Service’e “bu grubun üyeleri kim?” diye sorar. Sonra her üye için aktif bağlantı bilgisini bulur ve mesajı ilgili gateway’e yönlendirir.

Grup Boyutu Neden Sınırlanır?

Grup mesajlaşmasında üye sayısı arttıkça tek bir mesajın maliyeti de artar. 10 kişilik grupta bir mesaj 9 alıcıya gider. 500 kişilik grupta aynı mesaj 499 teslim operasyonu anlamına gelir.

Bu yüzden chat uygulamalarında grup boyutuna limit koymak mimari açıdan mantıklıdır. Böylece sistem gerçek zamanlı davranışını koruyabilir.

flowchart TD
    M["Grup mesajı gönderildi"] --> C{"Üye sayısı limit içinde mi?"}
    C -- "Evet" --> F["Anlık fan-out yap"]
    F --> D["Mesajı üyelere ilet"]
    C -- "Hayır" --> B["Mesajı kuyrukla veya reddet"]
    B --> E["Gönderene durum bildir"]

Bu diyagram grup büyüklüğünün mesaj gönderim stratejisini nasıl etkilediğini anlatıyor. Küçük ve orta ölçekli gruplarda anlık dağıtım yapılabilir; aşırı büyük gruplarda kuyruklama veya farklı dağıtım stratejileri gerekir.

Message Queue ve Retry Mekanizması

Dağıtık sistemlerde servisler her zaman ayakta olmayabilir. Group Service kısa süreli yanıt veremeyebilir, bir gateway düşebilir veya ağda gecikme olabilir. Bu noktada Message Queue kullanılır.

Message Queue, yani mesaj kuyruğu, işi hemen yapamıyorsan güvenli şekilde sıraya koymanı sağlar. Bunu şöyle hayal edebilirsin: mesaj bir görev olarak kuyruğa bırakılır, sistem uygun olduğunda tekrar işlemeyi dener.

flowchart LR
    S["Session Service"] --> Q["Message Queue"]
    Q --> W["Worker"]
    W --> G["Group Service"]
    W --> R{"Başarılı mı?"}
    R -- "Evet" --> OK["Gönderim tamamlandı"]
    R -- "Hayır" --> RT["Retry"]
    RT --> Q
    RT -. "Limit aşılırsa" .-> F["Başarısızlık bildir"]

Bu yapı sistemin geçici hatalara karşı daha dayanıklı olmasını sağlar. Retry sayısı aşıldığında istemciye başarısızlık bilgisi dönülebilir.

Verinin Nasıl Modellenebileceği

Bu tarz bir sistemde birkaç temel veri ilişkisi vardır. Kullanıcılar mesaj gönderir, mesajlar sohbetlere veya gruplara bağlıdır, grup üyelikleri ayrı tutulur, bağlantı bilgisi ise geçici bir session verisidir.

erDiagram
    USER ||--o{ MESSAGE : sends
    CHAT ||--o{ MESSAGE : contains
    GROUP ||--o{ GROUP_MEMBER : has
    USER ||--o{ GROUP_MEMBER : joins
    USER ||--o| SESSION : has
    MESSAGE ||--o{ MESSAGE_RECEIPT : tracks
    USER ||--o{ MESSAGE_RECEIPT : owns

    USER {
        string user_id
        string display_name
    }

    MESSAGE {
        string message_id
        string sender_id
        string chat_id
        string body
        datetime created_at
    }

    GROUP {
        string group_id
        string name
    }

    GROUP_MEMBER {
        string group_id
        string user_id
        datetime joined_at
    }

    SESSION {
        string user_id
        string gateway_id
        datetime connected_at
    }

    MESSAGE_RECEIPT {
        string message_id
        string user_id
        string status
        datetime updated_at
    }

Bu ER diyagramı sistemdeki ana varlıkları gösteriyor. Özellikle SESSION kalıcı kullanıcı verisinden ayrıdır; çünkü bağlantı bilgisi sık değişen, geçici bir bilgidir.

Yoğun Trafikte Önceliklendirme

Gerçek dünyada bazı anlarda mesaj trafiği normalin çok üstüne çıkar. Böyle zamanlarda sistemin her özelliği aynı öncelikte ele alması doğru değildir. Mesajın alınması ve kaydedilmesi en kritik iştir. Son görülme, okundu bilgisi veya ikincil bildirimler daha düşük önceliğe alınabilir.

flowchart TD
    T["Yük artışı algılandı"] --> P{"İş kritik mi?"}
    P -- "Mesaj gönderimi" --> H["Yüksek öncelik"]
    P -- "Mesaj kaydı" --> H
    P -- "Okundu bilgisi" --> L["Düşük öncelik"]
    P -- "Son görülme" --> L
    P -- "Teslim bildirimi" --> M["Orta öncelik"]

    H --> A["Hemen işle"]
    M --> B["Gecikmeli işle"]
    L --> C["Kuyruğa al veya geçici olarak azalt"]

Bu diyagram yoğunluk altında sistemin nasıl kontrollü şekilde sadeleşebileceğini gösteriyor. Amaç tüm sistemi durdurmak yerine en kritik kullanıcı deneyimini ayakta tutmaktır.

Özet

WhatsApp benzeri bir chat uygulamasında asıl mesele sadece mesaj göndermek değildir. Gateway katmanını hafif tutmak, kullanıcı bağlantılarını Session Service ile takip etmek, mesajları güvenli şekilde saklamak, WebSocket ile gerçek zamanlı iletişim kurmak ve grup mesajlarında kontrollü fan-out yapmak gerekir. Okundu bilgisi, teslim durumu ve son görülme gibi özellikler kullanıcı deneyimini zenginleştirir; ancak yüksek trafikte bu özelliklerin önceliği temel mesajlaşmanın gerisinde kalmalıdır. Sağlam bir tasarım, servisleri doğru ayırarak ve geçici hatalara karşı kuyruk/retry mekanizmaları kullanarak ölçeklenebilir hale gelir.

Bu yazı WHATSAPP System Design: Chat Messaging Systems for Interviews videosundan ilham alınarak yazılmıştır.


Kaynakça

Leave a Reply

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir