Skoro každá appka potřebuje dát uživateli vědět, že se něco stalo — „máš novou zprávu", „platba proběhla", „někdo komentoval". Zní to triviálně (pošli e-mail, ne?), ale dělat notifikace pořádně znamená řešit víc kanálů, opakované odesílání, preference uživatelů a to, abys lidi nezahltil. Tahle kapitola to dá dohromady — a hezky propojí, co už umíš z předchozích témat.
Pár pojmů na úvod
- Notifikace = zpráva, kterou systém pošle uživateli o nějaké události.
- Kanál (channel) = cesta, kterou notifikaci doručíš: e-mail, SMS, push (do mobilu), in-app (zvoneček v aplikaci).
- Fan-out = jedna událost → mnoho notifikací (komentář pod příspěvkem upozorní všechny sledující).
- Šablona (template) = předpis, jak má zpráva vypadat, do kterého se doplní konkrétní data.
Notifikace nikdy nedělej v requestu
Odeslání e-mailu nebo SMS závisí na cizí službě, která může být pomalá nebo dole. Kdybys to dělal přímo v requestu, uživatel by čekal a při selhání by mu spadla akce. Proto notifikace patří na pozadí: událost dej do fronty a odeslání nech background workeru. Tady se potkává skoro všechno z předchozích kapitol:
Co dobrý notifikační systém musí umět
- Víc kanálů — stejnou notifikaci umět poslat e-mailem, pushem i jako in-app, podle situace.
- Preference uživatele — respektovat, co uživatel chce dostávat (a hlavně co ne). Bez toho lidi odejdou nebo tě označí za spam.
- Idempotence — neposlat tu samou notifikaci dvakrát (Patterny). Fronta doručuje at-least-once, takže worker musí poznat „tohle už jsem poslal".
- Retry při selhání — když je e-mailová služba chvíli dole, zkus to znovu s backoffem, po pár marných pokusech to vzdej.
- Šablony — text zprávy drž v šablonách, ne natvrdo v kódu (jednodušší úpravy, překlady).
- Rate limiting / sdružování — nezahltit uživatele. Místo deseti e-mailů za minutu pošli jeden souhrn (digest). Souvisí s rate limitingem.
Fan-out: jedna událost, mnoho příjemců
U populárního příspěvku může jeden komentář vyvolat notifikaci pro tisíce sledujících. Generovat je všechny synchronně by zabilo request. Proto se fan-out dělá asynchronně přes frontu: událost se rozloží na mnoho jednotlivých notifikací, které workeři zpracují postupně. (Je to stejný princip jako fan-out u feedu v system designu.)
Co se děje PO odeslání (a o čem junior neví)
„Odeslal jsem e-mail" ≠ „doručil se". Senior notifikační systém řeší i druhou půlku:
- Doručitelnost (deliverability) a sender reputation. Když posíláš na neexistující adresy nebo na lidi, co tě označí za spam, poskytovatelé ti začnou e-maily blokovat. Proto se sleduje, jak to dopadlo. Základ, bez kterého ti pošta padá rovnou do spamu (a Gmail/Yahoo to od 2024 u hromadného odesílání vyžadují): nastavená SPF, DKIM a DMARC — záznamy, kterými prokážeš, že e-maily za tvou doménu posíláš opravdu ty.
- Bounces a complaints. Poskytovatel ti pošle zpět (často webhookem) info „tahle adresa neexistuje" (hard bounce) nebo „uživatel to nahlásil jako spam". Na tyhle adresy se musí přestat posílat — vede se suppression list (seznam adres, kam už neposílat).
- Tracking doručení. U důležitých notifikací chceš vědět stav (odesláno / doručeno / otevřeno / selhalo) — typicky zpětnými webhooky od poskytovatele.
Dvě věci navíc, na které se zapomíná
- Deduplikace napříč kanály. Idempotence (z Patternů) řeší „neposlat tu samou úlohu dvakrát". Tohle je jiný problém: neposlat uživateli e-mail + push + in-app o téže věci, nebo „už si to přečetl in-app → neposílej e-mail". To je logika nad jednotlivými kanály.
- In-app inbox není „jen další kanál". Za zvonečkem je vlastní datový model: notifikace jako řádky v databázi, stav přečteno/nepřečteno, počítadlo (badge), mazání. To je samostatný kus práce, ne jen „pošli zprávu".
Failure modes — jak to v praxi praská
- Odesílání v requestu → uživatel čeká na pomalou e-mailovou službu, při výpadku mu spadne akce.
- Žádná idempotence → uživateli přijde stejná notifikace třikrát.
- Ignorované preference → uživatel dostává, co nechce → odhlásí se nebo tě nahlásí jako spam.
- Notifikační bouře → událost vyvolá tisíce notifikací naráz a zahltí kanály i příjemce (řeší sdružování a rate limit).
- Tichá selhání → notifikace se neodešle a nikdo to neví (chybí monitoring).
🛠️ Cvičení
- Návrh pipeline. Nakresli cestu od události „někdo ti odpověděl" až po doručení e-mailem a pushem, s frontou a workerem.
- Idempotence. Worker dostane stejnou notifikační úlohu dvakrát. Navrhni, jak zajistíš, že e-mail odejde jen jednou.
- Fan-out. Příspěvek sleduje 5000 lidí a přijde komentář. Proč negenerovat 5000 notifikací synchronně a jak to udělat správně?
- Nezahltit. Uživateli by za minutu přišlo 12 notifikací. Navrhni, jak z toho uděláš jeden souhrn.
- Preference. Navrhni, jak uložíš a při odesílání zohledníš, že uživatel chce push, ale ne e-maily.
Náčrt řešení — rozbal, až si cvičení zkusíš sám
- Návrh pipeline — událost „někdo ti odpověděl" nezpracovávej v requestu, ale ulož ji do fronty; notifikační worker ji vytáhne, podle preferencí rozvětví na e-mail a push a každý kanál odešle přes svého poskytovatele. Pozor: request musí skončit hned (odeslání závisí na cizí pomalé službě), a worker odesílá s retry/backoffem, ne synchronně v cestě uživatele.
- Idempotence — worker si u každé úlohy nese ID události a před odesláním zkontroluje (např. v DB/Redis), jestli už pro tuhle dvojici událost+kanál neposlal; pokud ano, úlohu zahodí. Pozor: fronta doručuje at-least-once a worker může spadnout uprostřed, takže značku „posláno" zapisuj tak, ať obstojí i při opakování — nestačí spoléhat, že úloha přijde jen jednou.
- Fan-out — 5000 notifikací negeneruj synchronně, protože by to zabilo request a uživatel by čekal na celý průchod; místo toho ulož jednu událost do fronty a workeři ji rozloží na jednotlivé notifikace a zpracují postupně na pozadí. Pozor: jde o stejný princip jako fan-out u feedu — práci rozmělni, ať ji unesou workeři, ne request.
- Nezahltit — místo 12 samostatných notifikací je za krátké okno sdruž do jednoho souhrnu (digest): událost krátce podrž a po vypršení okna pošli jednu zprávu „máš 12 nových…". Pozor: souvisí to s rate limitingem — bez sdružování uživatele zahltíš, odhlásí se nebo tě nahlásí jako spam.
- Preference — ulož per uživatele a per kanál, co chce dostávat (např. tabulka uživatel × kanál s boolem), a worker před odesláním každý kanál proti preferencím ověří; e-mail v tomhle případě přeskočí, push pošle. Pozor: preference vyhodnocuj na serveru při odesílání (ne jen schovat tlačítko v UI), jinak uživateli pošleš, co si zakázal.
🧠 Otázky & odpovědi
Proč neodesílat notifikace přímo v requestu?
Odeslání e-mailu nebo SMS závisí na cizí službě, která může být pomalá nebo dole. V requestu by uživatel zbytečně čekal a při selhání by mu spadla akce. Proto se notifikace dávají do fronty a odesílá je background worker — uživatel je hned hotový a případné selhání se dá v klidu opakovat.
Proč musí být odesílání notifikací idempotentní?
Fronty doručují typicky at-least-once a worker může spadnout uprostřed, takže se notifikační úloha může zpracovat vícekrát. Bez idempotence to znamená, že uživateli přijde stejná notifikace dvakrát nebo třikrát. Worker proto podle ID události pozná „tohle už jsem poslal" a pošle to jen jednou.
Co je fan-out a proč ho dělat asynchronně?
Fan-out je situace, kdy jedna událost vyvolá mnoho notifikací (komentář upozorní všechny sledující). U populárního příspěvku to můžou být tisíce příjemců — generovat je synchronně by zabilo request. Proto se událost rozloží na jednotlivé notifikace a ty zpracují workeři na pozadí přes frontu, postupně.
Proč respektovat preference uživatele a sdružovat notifikace?
Když uživateli posíláš, co nechce, nebo ho zahltíš desítkami zpráv, odhlásí se nebo tě označí za spam — a o uživatele i o doručitelnost přijdeš. Proto respektuj, co a kterým kanálem chce dostávat, a místo mnoha zpráv za chvíli pošli jeden souhrn (digest). Méně, ale relevantních notifikací.
Jak souvisí notifikace s ostatními tématy?
Spojuje skoro vše: jdou přes frontu (Messaging), zpracovává je background worker, musí být idempotentní a opakovat s backoffem (Patterny), in-app notifikace se doručují realtime a sdružování souvisí s rate limitingem. Notifikace jsou dobrá ukázka, jak se předchozí témata skládají dohromady.
