Who Needs Kafka? Simple PubSub on Postgres Using LISTEN/NOTIFY

Paul Whalen

whoami

  • Senior Staff Software Engineer @ PEAK6 Capital Management
    • Proprietary options trading firm in Chicago
  • ~10 YOE as primarily-backend application engineer (Java, Go, Python)
    • Data engineering experience as well, with lots of Kafka and SQL
  • Database nerd

Agenda

  • PubSub in Theory
    • On Kafka
    • On Postgres, with LISTEN/NOTIFY
  • LISTEN/NOTIFY in Practice
    • Config blob service
    • Shopping cart
    • Time series server
    • Ticker viewer
  • Summary of patterns and gotchas

PubSub in Theory

What is PubSub?

  • A messaging pattern
  • Publishers send messages for a topic
  • Subscribers receive messages on a topic
  • Subscribers don’t know about publishers, publishers don’t know about subscribers
    • Something in the middle (a “broker”) connects the two
  • Optionally:
    • Usually real time
    • Persistent or ephemeral
    • “Queue” behavior, marking messages as processed

What is PubSub?

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1cXFlT20pcdTAwMTN9z6+gfF9j3dl6lryxb8FcdTAwMDFcdTAwMDJcdPDVrZRsy7aCN2SZ7Vb+++1cdTAwMTFgyZZkbFx1MDAxMOCvwFSlQFx1MDAxYUmt6e5zTveM8++npaVSeNP3Sl+WSt51zW379cC9Kn22xy+9YOD3uniKRX9cdTAwMGZ6w6BcdTAwMTaNbIVhf/Dl77/jK5xar3N3ldf2Ol43XHUwMDFj4Lj/4d9LS/9G/+JcdTAwMTm/bq89qITeSlx1MDAxN1x1MDAwMjhZ0z9cdTAwMGZ3hlx1MDAxYrv1m0p0aTTowZjAq4Vut9n24lPX1lx1MDAxMlCOVIJcbsJAMzAwOntjz3LqUKaBclx1MDAwMkyA4KOzV349bOFcYiqloyjXRilcdTAwMDJEaVx1MDAxYd+g5fnNVohjgDlcdTAwMDQkUFxyIKRWwoyG3Fx1MDAxOfRliYyOXGbCoHfurfbavcBa/Vx1MDAxNzWK1lhsc9WtnTeD3rBbXHUwMDFmjVx0XHUwMDAztzvou1x1MDAwMU5SPK7ht9vfw5vo7jjROKmliWf8vH9cdTAwMDM2cTzvKnxos9X1XHUwMDA21lx1MDAxMXR0tNd3a35o54qS+C2shf3teuSzf2KbXHUwMDAyt+NtW6d1h+326LDfrXvWXHUwMDE1JZeMPa1bv3/ag8Njb/L7I39i2z3P3pgyo4lRROrRmTjqtJg8WOl1o1x1MDAwMKRcdTAwMDRcdTAwMDC0kiqeXGZ/sIaRXHUwMDE3RndtuO2BXHUwMDE3u8Catj5cdTAwMTmVychcdTAwMWNcdTAwMGK80LuO/ZKI2+Nf5crq+cVBZXe5fLmrqu2DjeZ1aTTuz+fs295dXHUwMDFjnNdcdTAwMDa+OL5Sp7VO6Fx1MDAwZoas2a9djj/l4fluXHUwMDEw9K5cdTAwMTL3vf8tdsuwX3fv3pMqLlxyTiAzksQubvvd80mftXu183hqPiVcZp7Izuy3TGXn2CTdJaahXHUwMDBl0Vx1MDAxNDOPXHUwMDEzXHUwMDAzzFx1MDAxMDGemUI4XHUwMDEypFKMM6PQZplKTWNcdTAwMWNtjKFKYm5zk1x1MDAxODHKTFx1MDAwNlx1MDAxZqmYk4o/snNxbPh90mmhJVx1MDAwM1x1MDAwMSwj53huzlx1MDAxOabQO4TKp6RcXJHRXHUwMDFiXHUwMDA3o1xyQnz5/rDa9lx1MDAwNy0vWKJcdC/2uuF3/9ZazsjY0Vxyt+O37bTD2J2W237TzkCphkZ7QSk5XHUwMDBmoY88N1x1MDAxYdDx6/UkL9Xwpq7f9YLtWVxirlx1MDAxN/hNv+u2j6ZZ71x1MDAwZcPeoTe4sz9cZoZecn68rYd8oFx1MDAwZYMpyVxcPzKrqrve27zgV63Kkfy61rhozEy1QJXDXHUwMDE1U1xueVx1MDAxNtE2QaaLQbVcdTAwMWXhlNN3kt/s+VTLXHS6XHUwMDAygGelPVx1MDAxNSlcdTAwMDJcdTAwMWVxLVCQRiv1pMR/XHUwMDFh1/5mhycr9fPaps9Ej1xy9yt8++evV+DaqfdcdTAwMWRuXmxcdTAwMWVvLJ+0bk9cdTAwMGZ/XHUwMDA0rN86WYZcdTAwMWaFcTgnKuGZZ3F49uzNwOGAXHUwMDE5XHUwMDBiNkiIJFKCnlx1MDAxMNePU7jUjmVcdOBcdTAwMDRRXHUwMDFkcd2kM34uXG5/VynOZ2dwTFdji1x1MDAxZpli68iU3FxcVoZcdTAwMGIlKS+cw+eN3lx1MDAxNIeHvb5fW3JcdTAwMTeBv1x1MDAxZmHNSf5OWV5cZnfLmrvl9m5bZ1e/XHUwMDFilf2Lr6fLR3A8O3cz4lxiTinivVx1MDAwMJKQV3beuOJcdTAwMGVcdTAwMTVMo1x1MDAwMkT6Rnr+oO6XzGvxfOqW6CpcdTAwMDMkkVxiiXSXMHl0RN2Y75id5mn5/jTq3lq/uL3k10Dquz87p79cdTAwMWGbuv/156xcdTAwMTRb+V7fqlaWmztcdTAwMDd1n9dcdTAwMDJmyv7guFx1MDAwMOo+OT/s7/BgeaNG13fWbze+dap+t4j7drdbXm1cci7LXHUwMDA39YOj/X2+d8xcdTAwMGWKk1x1MDAwNIYk+lvPklx1MDAwNNlemUlcdTAwMTJgVVx1MDAwZZpcbpSAXHUwMDA0hIqzPEJcdTAwMTItXHUwMDFkqSglXHUwMDE4n9p25dJQXCJcdTAwMTVej5JAaM5cclx1MDAxNlx1MDAxMTE3fUiCR6FcdTAwMDNml1x1MDAwNJIgXHUwMDA0XHUwMDBiqrNcdTAwMWFplMnJoyOIMFxmY42LhP4vSlx1MDAxMsxcdTAwMTm9OZKgulxikuBcdTAwMTEyzpZcdTAwMDTVoiWBd8Ga5e5XuTpcYi9ub9qw0a5src3eOZd4e61sXHUwMDE2Wp/DuLh/e03wrtp18tmawMo6m7mZ1TzN7eJJXHUwMDAwhSVcdTAwMDCLXHUwMDA3vLhcIlj/dlbt9L1fZUXOyvz60KWNxvYrKIJZXHUwMDE4XHUwMDE2XHRNJHPx6Vxmm/2WMzCsbZxrXCI41nNSKCy/5XhiPk6xlFx1MDAwMFx1MDAwZTFYXHUwMDExgkTMpYynM/Ojc56XimqOxjnRnEqjsnJOq7yU41xcXHUwMDEzSUA/KeWKjN5cdTAwMTTDxq1ntlxiLPtcYr/lN85Z0Ux7omFrx1v7SlTr8vBb57S1QvXlzEyrlHKIooJKZlx1MDAxN8LMePX99o3zd5Xe+vnVN0pZJkGl22pRLuZKa6mNVlK/JtW2glx1MDAxM3e91r6tN1qNo6NcdTAwMTPdON6/XHUwMDBl3rpI/r/pm2fP3lxmXHUwMDE0jlY4VjujxFx1MDAxNlxi9WLupW9KuWOMXWSxXXOtMlx1MDAxMv6DwPMy3MxO4IxQRkFDdipPWVx1MDAwM5Ncblx1MDAxOGUvweHPbZxcdTAwMGaG1UEt8KuLsvr9XGJ1TpJ4tvnFsPjlt5Wbbnh63T+rLNPdwe3u2THfm4PFjVx1MDAwM1xuXGKlmE2cyMl6mTnIz0Jzwpg2hH5cdTAwMTTML5rky1x1MDAwNdA4oHZcdTAwMTcsc9tcdTAwMGKT+TRcdTAwMWVtNOTySVxysqfReHh2eLJcdTAwMTf0fu+u7jf33I29nUrQlIvRk6ZYqFx1MDAxNrTVLPstZ6Rbalx1MDAxMMutxobJZWpbMFx1MDAwM1x1MDAwMSWYXYXm3KRcdTAwMTOTXHUwMDFhXHUwMDA3Sydt+9qo61x1MDAwNE3n5Vx1MDAwN9vmJeLKXHUwMDFjq9SEUG71TtayXHUwMDE1S0x7im0xT1x1MDAxNWOm8GXqeaN3XHUwMDFh2y5EyfxcYsVNYdvCa+bpnbpcdMxcdTAwMTnLZ8GMg6HCuFx1MDAwMuRKdNW4fuZcdTAwMDaJWGCu2i3fXG5EYlXhIaM10iglgoBcdTAwMDGFNVd8fStpPGVSXHUwMDAw3p/YXHUwMDFlzjz57dmfd5Lfq7NcdTAwMTItyyNaTYhAL2V2pk3uWjUwJdCHiVx1MDAxN3jrLlm/50+yePzbUlx1MDAxYy/RXHUwMDFmo9//+Zw5Oi9C7SdcdTAwMTWb8c1SjNx2XHUwMDA34Wqv0/FDfMt9a2FcbnxDN1xiV9Chfrc57rj7b2bM0myL4Ks2tG9fJlx1MDAwZeFcXFhcdTAwMTFMgVx1MDAxMVx1MDAwNTQuT22Yuf1kMEZhfz1uWipQvG79cVx1MDAwM6evuY1cdTAwMWKIpVx1MDAxOcpoQTmqXHUwMDAxW0c/075oXG6XLVC1PDeVK2h98twkonntau9qJlx1MDAwMTR9o+J0xFRcdTAwMGWnhlx1MDAxME1cdTAwMTVcdTAwMDaTmPxcdTAwMWFcZmj0XHUwMDE5hpRkkmC1m24xauooJlx1MDAxNd6CMiGT9fBcYjGJo4SQXFwzxTTTXHUwMDFjXHUwMDEyK/dcdTAwMWaQOVx1MDAwNplrz4ZMXHUwMDFiv1xm0SHdTIwkRW5tYiUu4YQ/6Wswj+3Jf1JboljQzItS+ymnXHUwMDAz9DVwc/ru/lx1MDAwNCwhbEpcdTAwMTQ0TFxirFx1MDAxZIFcdK5cdTAwMTKD7lCJOYbbbz8x1KVcdTAwMDSxP4lbXHUwMDA1oej0zYzj5jKuhUCTXHUwMDA1YVxupCZcImXwQsLo9L71NFx1MDAxOJVaOFx1MDAwMkNLME1cZuqPXHTdqVx1MDAxMGTRedxcYi2USnb6RzBqR0ipXHUwMDE5klx1MDAwZd4so8FDqUJ2slx1MDAxM4pcdTAwMGXmmuq5OjzvXHRF11x1MDAwYkBRYoiUUmTtikY/5aKo0oyiTiy8uVx1MDAwYpRCzJpvhqI5QWo/5XR8vlx1MDAwNojOrO2Ig1x1MDAxMoYyhiBJsT5g8WrpS0rP6T3sMfM0SnpUxFx1MDAxOHeow0hcdTAwMDZkXHUwMDAyXG41Zr9cdTAwMGUpuN3BXHUwMDFiR+FcIkHo9CW66Vx1MDAxMEpcdTAwMWMwXHUwMDE0VaZEXHUwMDBlg/G9K1xmpGNcZiNY2FMhXHUwMDEzs1x1MDAxM3/nXHUwMDEzKVx1MDAxMOt6XHJcdTAwMWHp3SRTMKFDXHUwMDAxg1coKam2M1xyXHUwMDFmpXtcdTAwMGWCbjxcdTAwMWZBjdIo/jjLRFCVv9FcXDKllKGy8Op9MYRobphGV6dcdTAwMDL0NSB0ZmFXvlN2iJ9aXHUwMDE5wTVcYlxuSVx1MDAxMLqDKe5obnNUXHSlJfIhm1x1MDAwZqeKhdWo5Vx1MDAwMFx1MDAxNKW9XU1XXFxcdG3SyMpcdTAwMWRQiClCRaJVmfnU82uJ06mrMdOR1ThcboGTXHUwMDEwhszCxfguXCLkQlxmO46KXHUwMDA3mVx1MDAxYuklo8SXjpZcdTAwMWG9LqJcciw6a1OgXHUwMDAzRFBtNyEpxaj80KY5yLr5bGRF7pdcXHHIKvCTXHKa1H90oVx1MDAxMJJRYVx1MDAxNF7h203A8WPfTJvmXHUwMDA1qf2UU/G5YNpcdTAwMTThSSCkXG5cdTAwMGJUQoJJXGZ6KPClXYRUdlx1MDAxZFxmhNHyXHUwMDA1UHX6XHUwMDEy0Ji9XHUwMDA0p1x1MDAxMz9cdTAwMDTZyjBuO05cdTAwMTlcdTAwMTZcdTAwMWKOIcKAMaRcdTAwMGJcdTAwMTBz8kDxqPrp3sklt9//XHUwMDFlYlx1MDAxMI+monTpe1cradj5q1x1MDAxMX3sunZkmIVcdTAwMWMvStc/n/78XHUwMDA3+KHIVCJ9publisher 1topic atopic bpublisher 2subscriber 1subscriber 2

Kafka as an implementation of PubSub

  • Producers publish messages on a topic to broker
    • Messages are written in order, and assigned IDs (“offset”)
  • Consumers subscribe to a topic and recieve messages from broker
    • Read from an offset
eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1cXFtT20hcdTAwMTN9z6+g2NegnVvPZd9sLvlgXHRXXHUwMDEzXHUwMDAyX22lhC2wgi1cdTAwMTlZ5pKt/PftXHUwMDEx2JItyfFFSajCTlVcbqSR3DPT5/Tpnlx1MDAxOf59t7a2XHUwMDFlP/W89b/W1r3HptvxW5H7sP7eXr/3or5cdTAwMWZcdTAwMDZ4iyW/98NB1ExatuO41//rzz/TJ5xm2H1+yut4XS+I+9ju//j72tq/yf94x2/ZZ5/Y36SmXHUwMDFhf1/XWo2nXHUwMDAz/dDs9i43kkeTRiNjOlx1MDAxZL/X99JcdTAwMWKPeJVr6khjtCZMaqY4XHUwMDFi3X3Cu6CIXHUwMDAzTFx1MDAxOdDEXGKlQYzuPvituI0thCCOkqC0ppyBVoKPmrQ9/6ZcdTAwMWTbvlx1MDAwMnG4kFx1MDAxMqRcdTAwMTKaXHRt5KiNXHUwMDFi3HSscWR0pVx1MDAxZkfhrbdcdTAwMTl2wshcdTAwMWH9XHUwMDA3uzaeXHUwMDEwqdFXbvP2JlxuXHUwMDA3QWvUJo7coN9zI1x1MDAxY6O03bXf6ZzGT8nbcZxxTNcnvuP8pVx1MDAwYmzietlT+KU37cDr23mgo6thz236sVx1MDAxZCxK0l5YXHUwMDBie7utZMr+SW2K3K63a+csXHUwMDE4dDqjy37Q8uxcXKxfPo19W9B6+bbhfKeTyV6ufE9t9zz7YmYoxXmCtFNcdTAwMTmfk3Ty6kFcdTAwMTgk/kdcdTAwMDVVVFx1MDAwYpb2wO9vod/FyUuv3U7fS2fAWrY96ZNZv1x1MDAxY3O72HtMpyXjtSfbO3rP3Pl7x+1cdTAwMDNxOGh0XHUwMDFlL3b99VG77y8/paM36LXcZ3uo4tZZXHTTRKX+1vGD28mh7YTN27RcdTAwMGLvMkM2gaFia3JcdTAwMThcdTAwMWHrTFx1MDAwMiBQ1OFMXHUwMDAzXHUwMDAwNcDwM1x1MDAwNiBJmUNccuFKUaBayVx1MDAxY34kdTQ3Rlx1MDAxMlBAsZ0sgs9cbi7FcPlWXGaXsdZDXFxQpo3hiuhcImBwKFx1MDAwNVx1MDAwNlx1MDAwNSE4M2IhZFTpvKkvWlx1MDAxZsTuX9nRjzJTXHUwMDE4XHUwMDA28an/LaFcdTAwMDcydnXH7fqdhM/HXlLr+De2++tNtDf7Jlx1MDAxY4PYxzg0alx1MDAxMIe99G5cdTAwMTPf6PqBXHUwMDE37c5cdTAwMTJ9wsi/8Vx1MDAwM7fTKLHaXHUwMDFkxOGJ13+2O45cdTAwMDZedki8/1xyQUBcdTAwMWRcdTAwMDZT4Pt40Fx1MDAxNmfH1N0h13u1jiHbov34KVx1MDAwZt/Ia8bPXHUwMDE4XHUwMDFhwzDVzJE40aDBXHUwMDEwQoCl85KAWFx1MDAxM4dibKRKSyRJSO9cdTAwMGVRTKV0XHUwMDE05dooRYAoTdOBXHUwMDFlwVx1MDAxOJhDQFwiXHUwMDBiXHUwMDAwXGKJgdLMgWpqXHUwMDE0bbK3gWr3YNYgyMuCoGRGXCLItMh69Fx1MDAwYtYpM5NX0yCIXGZueLZcdTAwMGI/PVxunn+93rqNL2ntzr+/j2p3XHJVZ14mXG6+L37ty8PukVx1MDAxNLtXpydbXHUwMDFi6uC+12xcdTAwMDQ3W2r8W4bf70ZR+DBPdJWGXHUwMDE4k/HjpaJrcS9niK6McccoJjiqXHUwMDEzYJJcdTAwMTExjkzDXHUwMDFkiaFcdTAwMTfDKzdUXHUwMDFimkOmxufxI1xiUFx1MDAwMVJcdTAwMDDNXHUwMDAzc674+qaQeDhPfFx1MDAxNZJz0KZIeGpVXG45XHUwMDAwKYlQVC5cdTAwMDK5Kr03XHUwMDE3XntR2Fx1MDAxYTR/UYDt+q1WNipNxNhcdTAwMWaEt8lcdTAwMTibN72aKHtcdTAwMThFXHUwMDFih9BcdTAwMTRf1TfCvF7rcqu5cTVzlFx1MDAxNcw4lChMXHUwMDA0XHUwMDE1XHUwMDAzoWXqXHUwMDE0z0GWOsJglFx1MDAwNdBCilSmXHKRzDGAamaYwSbEIOZcdTAwMGJCrMA8k2oqMVtFTYeSelx1MDAwZWR7hFNO31xiso+WjrGUSEVcdTAwMDTnolBQ01x1MDAxY1xyXGZcdTAwMTHPiJJaUClSJl86yC5cdTAwMWRccqdG2eDCO+3u8+3+5uPJ10v3POLXje2qoqwySJkqi8bFoyzbXGZPd+6hebT3eevb9sndvr+97c5cZk8tXHKiX2NcdTAwMTTlQHku0mph0Yf3XHUwMDE1VcB0PpNdaeBcbvF5sjw+NTFUcFwiSVx1MDAxMT5VTlx1MDAxYVx1MDAwZvFcdMYgdbIq4fkjXHJMLmqHdX5eJ/tBv3Zz+vFLa3fv+lWg09iYwKpBZ3EvZ9DAKGVcdTAwMWOCoJEoaFDsZnD1LIG1w0FpgYJHaJTKeVx0rFx1MDAxY7xnuK3hYpZrRFx1MDAxZZcrXHRcXFx1MDAwNsTT2SUw0p4yoqTCXHUwMDA0pVx1MDAxNSbGKXIqV2xcdTAwMTHAVem7OVx1MDAwNYxcIrQ/6L5cdTAwMGVcdTAwMDX8g9g2qYDzplejgKdrjFx0plx1MDAxOUOxla9MXHUwMDEyziSqV27IhPxFt3GIXHUwMDE2hjOMsEzRNDJcdTAwMGVhrMDRmChcdMKsvpUkndpcdTAwMTGMiYO3OVx1MDAwMcS4XHUwMDA0Llx1MDAwMMhcXLj27L83guvG0istXHUwMDAwXGaTXijUv4i80oyX4eQpRjM08bsz3l7oT8bv9Ke11GOSX0Y///O+sHWpmyZP51x1MDAxZDR9Xy5cdTAwMWN33H68XHUwMDE5drt+jFx1MDAxZD2yRuaYN3ajuI6z6lx1MDAwNzfjs/eyZjpL+pwwWHPQT1x1MDAwMIS4XHUwMDA0g+pcdTAwMDeHXHUwMDE0RVx1MDAxMOhMo1x1MDAxYrdn/cExnFx1MDAxOIXdQ3GlOMm2uPZcdTAwMWbHXHLN+Y5cdTAwMTe0fmzu9DR73FxcxrVcdTAwMTBoMo63kpSYnL10LvuSXHUwMDAxrVn2antuXHUwMDBlPmh99t4kzXmdq/BhJi00XbRNo1GlwcFcdTAwMDSSMbuSzClcdTAwMTlfblOUWK0jbVx1MDAxNYBcdTAwMTOh82JIXHUwMDE5x1BcdTAwMDSKUfhcdTAwMGUgopBFMXdcdTAwMDG7oC1cdLqozEiqXHUwMDE1iY6R6NnSJCpcYk5cdTAwMDOnrKhSXHUwMDBmXCKXuoxI1Kag+J+ueFXuWTQttCpXLYmWeWnydM4/f1x1MDAwNYfOTEpcdTAwMWLPrESo0iiHuVx1MDAwNpFcdTAwMDbDXHUwMDExK3FHcyokUUJpaTsps2+qhkanS8ZcdIttzUMzYmvRXFxcdC1lgcmgXGJBsk34Vpn5iP9cdTAwMTdcdTAwMTFruPGlrT9cdTAwMGVOa19DXHUwMDEzN+VOg15cXESzJJnJNlx1MDAwNs1wXHUwMDEwNOaRJu3dc5IplIPzxCWOXGZDr0xlxmhcdTAwMWaDdpgyXHUwMDA2OCG2RitT/bpYkvmmqrGfZk8yXHUwMDAxc1x1MDAwN405f6HqLC26Uq2VkEi2i1R1xszIXHUwMDE3MLXRc1x1MDAxNTBzaWZcdTAwMWP2/Oaa+1xusswxqydTypyZ1WSU9EON7ana09nxVuifXHUwMDBmoovjzfplXHUwMDFlsfaVXHUwMDEzyymGOkZcdTAwMTNBXHUwMDAxgclRXHUwMDEzjSNWXHUwMDBix1x1MDAwMMVcdTAwMDZcdTAwMWNcdTAwMTQok9+zQFx1MDAxYymVMFJqxLSShlx1MDAxN9SF7MY9LVx1MDAwMSlcdTAwMDE1Mcac1P9XXGJcdTAwMWVD8PnySlxiXHUwMDA1K1NcbmTWeYdKqLx4pFDpXHUwMDEyyVx1MDAxNkkmp+NaY5KTWS3/XUqo3EvtJ+efVSqhyVxiXXJnrsg+0lx1MDAwNiXbmL41d7tcdTAwMTdHW6JBN+/qOoiC3vbJLGSAmbSDmtgmpIJxXHUwMDBlk1x1MDAxYpgkilx1MDAxN45ccqTExDufXHUwMDE1rbigQi74vDRcdTAwMTfgWFntXGa0iFxm8rua0qVVQsBOcNWlZG2rNr+/tPS2uODefFRnaieoK/k5ilx1MDAxZU59Rs7jWbhAUlx1MDAxY1x0zFBcYrKBgOze1mcuUFx1MDAwZVx1MDAwMDKBxshhXHUwMDE45Fx1MDAxN3JXZFAhXHUwMDE5XFwsT1x1MDAwNkJTzYUyhWRQWlwiXHUwMDExXHUwMDFh51x1MDAxNn5cdTAwMDZcdTAwMTcoRVO5seKCX8FcdTAwMDVfOk+3N1x1MDAxZsg2OXRcdTAwMWYl4kKz/XrBslNcdTAwMDFcdTAwMTco5UjMymWyqYOSyT1XXHUwMDEymUJgyo66XHUwMDEz87iVMPipXFxwuTxcdTAwMTdcdTAwMTiBeTyh+cqo5YLyQ1xmmCBwbYAttKljXHUwMDFhXHUwMDE5MFwi6ULJ/4pcZlx1MDAxNiZcdTAwMDO6xVx1MDAxYj2+v/M12N85XG66jN/G9YLjfnkyUEzao0pCYJKggGROgz2TXHUwMDAxd+zCiVx1MDAwMa1Qf4r8XG70ilxmKiRcdTAwMDO3XHUwMDAyMkB2T06sXHUwMDE0kFx1MDAwMS3fgWnZwGjFXHUwMDE2Klx1MDAwNk5nXHUwMDAzyFZcdTAwMThXbPAr2OBuY+fvL7d7vH+2u+W1P9/u94698zxcdTAwMWLkK/5cdTAwMDK4o9FcdTAwMTGIoYCZJp1YSSXEXHUwMDExOJ+UUFxymkB+PzbljtCYcVwij1x1MDAwMKWmkFxyViX/MvhfXHUwMDE1w7+o5K+BXHUwMDAyIaJwI6cqr1xmoo9TvuCJ3mkgN1x1MDAwMsxCdcFhvZ+89kp/xsBqavyb9a2HT1x1MDAxYudccnlcdTAwMTleNc5M+2j75K7gdGLBqlx1MDAxY1VcdTAwMGVTVHCuQCGVTe5cdTAwMTn7XHUwMDExRrVcdTAwMDPcbv1kWlK7s6xgt8NcbqJlXHUwMDEwbc5cdTAwMGVRu3aMsopcdTAwMTfu/ZxWsKNcZpW5lFXvXHUwMDA1W1x1MDAxZaT0tYOUVlxy0lx1MDAwNt17uNzU7V2jni427+82zrqxnFx1MDAwNaQomlx1MDAxZJxDYvdkaIuPeVx1MDAwMylzKNg/wSFcYudcZlx1MDAxMbZC6Vx1MDAxYyhtzY5Smfxcclx1MDAwNlpYVcfpKUUpammcXFxe9Z9cdTAwMDCoXHUwMDAwpfy1o5RXjdKPg/vHjd1wL/CVaNe7x5u7XHUwMDFm4q+zbXCRySF+jIbApFDjx5tmQOlK3s6BSm+O2ClcdTAwMTVD2jPFNa3yk8NcdTAwMDSMXHUwMDE0XHUwMDA2oOqTw9rYI27LoJK9dlSyJVD57iVBXnd7vdNcdTAwMTiHbn1YtFi/972Het6r/7hOPvbgVYJp69FeUuv4/u77fymDY1x1MDAxMCJ9brokerproducerconsumertopic a0132

Kafka key points

  • Messages are persisted
  • Consumer can read older messages
  • Consumers process every message
  • Consumers read the entire message

Postgres LISTEN/NOTIFY

  • NOTIFY is the “publish” command (writer)
  • LISTEN is the “subscribe” command (reader)
  • NOTIFY sends a message on a channel
  • Any connection currently LISTENing on that channel receives the message

Publisher:

SELECT pg_notify('a_channel', 'hello world');

Subscriber:

LISTEN a_channel;

“Kafka” on Postgres

  • A table per topic
  • Publisher writes to table, NOTIFY ID
  • Susbcriber LISTEN, reads messages from table

“Kafka” on Postgres

Table to store messages

CREATE TABLE a_topic (
    id SERIAL PRIMARY KEY,
    message TEXT
)

Subscriber listens

LISTEN a_topic;

Publisher writes message…

INSERT INTO a_topic (message)
VALUES ('hi')
RETURNING id

… and notifies that it has been written (transactionally)

SELECT pg_notify('a_topic', '1')

“Kafka” on Postgres

Subscriber receives notification…

'1'

… and reads message

SELECT * FROM a_topic WHERE id = 1
id message
1 hi

LISTEN/NOTIFY in Production

Config Blob Service

Config Blob Service

  • A service that provides create/read/update/delete storage for a blob
  • Updates are pushed out immediately to the front end
  • The configuration is opaque to the DB, understood only by the application
CREATE TABLE profile_config
(
    profile_id  SERIAL PRIMARY KEY,
    modified_at TIMESTAMPTZ DEFAULT now(),
    config      JSON
)

Writing Data

Example config blob

SELECT json_build_object(
    'profile_id', 1,
    'config_key', 'config_val'
) AS data
data
{‘profile_id’: 1, ‘config_key’: ‘config_val’}

Writing Data

Insert blob

INSERT INTO profile_config (modified_at, config)
VALUES (
    now(),
    json_build_object(
        'profile_id', 1,
        'config_key', 'config_val'
    )
)

Notify config_update channel with contents of blob

SELECT pg_notify(
    'config_update',
    json_build_object(
        'profile_id', 1,
        'config_key', 'config_val'
    )::text
)

Reading Data

Subscriber LISTENs (before data was written)

LISTEN config_update;

Receives

{‘profile_id’: 1, ‘config_key’: ‘config_val’}

there’s no SELECT!

Cool!

  • Faster
  • Less indirection
  • Like Kafka: only receive, not the notify then read

Large write

But what if the newly written config blob is big?

SELECT pg_notify(
    'config_update',
    json_build_object(
        'profile_id', 1,
        'config_key', REPEAT('a', 10000)
    )::text
)
---------------------------------------------------------------------------
InvalidParameterValue                     Traceback (most recent call last)
Cell In[10], line 1
----> 1 run_sql("""
      2 SELECT pg_notify(
      3    'config_update',
      4    json_build_object(
      5        'profile_id', 1,
      6        'config_key', REPEAT('a', 10000)
      7    )::text
      8 )
      9 """, print_sql=True, print_output=False)

Cell In[1], line 34, in run_sql(sql, print_sql, print_output, client)
     32 cursor = conn.cursor()
     33 cursor.execute("SET schema 'pubsub'")
---> 34 cursor.execute(sql)
     35 if print_output:
     36     fetch_and_print_md_table(cursor)

InvalidParameterValue: payload string too long

Large write problems

  • ❗ Gotcha ❗: NOTIFY payloads have a hard 8kb limit
  • You must know the data you’re putting in the payload
    • Don’t accept untrusted data
    • In our case, it wasn’t malicious, and it was only just barely over

Large writes

  • Kafka has a message limit size too, but it is much larger (1MB)
    • (with config, you can get it up over 100MB)
  • I have also been the idiot writing large messages in Kafka
  • In Postgres, you can decouple the notification from the read/write
  • 🟢 Pattern 🟢: NOTIFY instructions about what to read, not the data itself
    • Many times, this is the ID of a row, but not always…

Shopping Cart

Shopping Cart

  • A service that manages an online shopping cart (e.g. Amazon)
  • When an item is added to the cart, a row is added to the table
  • A front end is connecting to one instance of the server that is LISTENing to postgres to show the updated cart
    • There are multiple instances of the service

Shopping Cart Schema

CREATE TABLE shopping_cart
(
    id       SERIAL PRIMARY KEY,
    item     TEXT NOT NULL,
    username TEXT NOT NULL
)

Adding an item to the cart

Add item

INSERT INTO shopping_cart (item, username)
VALUES ('iPhone', 'paul')

Rather than NOTIFYing the ID, we NOTIFY for the user:

SELECT pg_notify('shopping_cart', 'paul')

🟢 Pattern 🟢: NOTIFY instructions about what to read, not the data itself

Reading the updated cart

Listening

LISTEN shopping_cart;  -- 'paul'

Read the entire cart for the user

SELECT *
FROM shopping_cart
WHERE username = 'paul'
id item username
1 iPhone paul

Querying for the whole cart

  • Querying for the entire shopping cart is easiest
    • Service doesn’t need to track to cart, it just refreshes it
    • The whole cart is never prohibitively large
  • With Kafka, you get all the messages
    • Must track the state of the cart in the service
  • 🟢 Pattern 🟢: On LISTEN, query for the whole state
    • Let the database manage the state
  • Stateful stream processing with Kafka:
    • The greatest mistake of my career

Not querying at all

  • A given user front end is only connected to one instance
  • We only need to read the cart if the user is connected to that instance
  • With Kafka, you have to receive the contents of all messages
    • Exception: partitions
    • Partitions can be clumsly; you can only partition one way

Bonus: Handling deletes

Add a column representing items that were removed from the cart

ALTER TABLE shopping_cart
    ADD COLUMN removed BOOLEAN DEFAULT FALSE

User removes from cart

UPDATE shopping_cart
SET removed = true
WHERE ID = 1
SELECT pg_notify('shopping_cart', 'paul')
SELECT *
FROM shopping_cart
WHERE username = 'paul'
AND NOT removed

… there’s nothing in the cart!

🟢 Pattern 🟢: Query for the whole state, let the database manage the state

Deletes in Kafka

  • “Tombstone” messages indicating delete
  • Application must update state to handle delete
    • Often surprisingly hard, especially when it involves outputting more tombstone messages

Time Series Server

Time Series Server

  • Time series storage of stock prices throughout the day
  • Traders want to see up-to-date aggregations, like min/max/avg
CREATE TABLE stock_price
(
    ticker    TEXT,
    timestamp TIMESTAMPTZ,
    price     NUMERIC
)

A write-heavy workload

Stock price changes a lot

INSERT INTO stock_price (ticker, timestamp, price)
VALUES ('AAPL', '2025-01-01 8:30:00', 100.00),
       ('AAPL', '2025-01-01 8:30:01', 100.01),
       ('AAPL', '2025-01-01 8:30:02', 100.02)
SELECT pg_notify('stock_price', 'AAPL');
SELECT pg_notify('stock_price', 'AAPL');
SELECT pg_notify('stock_price', 'AAPL')
LISTEN stock_price;
'AAPL'
'AAPL'
'AAPL'

Serving up-to-date aggregations

On LISTEN, we do this aggregation query 3 times

SELECT ticker, avg(price) FROM stock_price
GROUP BY ticker
ticker avg
AAPL 100.0100000000000000
  • This query could be intensive with a lot of data
  • Queries could get backed up behind each other on thread the service is using to LISTEN
  • ❗ Gotcha ❗: The LISTEN thread can get backed up
  • 🟢 Pattern 🟢: Do your work elsewhere from the LISTEN thread
  • If stock prices are changing faster than the user can see anyway, do you need to keep up?
  • 🟢 Pattern 🟢: LISTENer doesn’t need to care about every message

Ticker View

Ticker View

  • Traders are constantly switching between stock tickers to view data related to a given ticker
  • When a trader switches what ticker they’re viewing, each UI receives a message from the service with the ticker to load data for

Ticker View Architecture

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1cXFtT20hcdTAwMTN9z6+g2NdFOz2Xnpl9XHUwMDAzXGZcdMstX+xAyO5WSlx1MDAxONkoXHUwMDE428hcdTAwMDJcZlv571+PXHUwMDAxS5ZkI18grlxuTopcdTAwMDJJlnpm+vQ5PdOj/96trKzGd91g9c+V1aBf91vhWeTfrv7ujt9cdTAwMDRRL+y06Vx1MDAxNFx1MDAxZvzd61xcR/XBledx3O39+cdcdTAwMWbJN7x65/LhW0EruFxm2nGPrvub/l5Z+W/wk86EZ+6721x1MDAwN7v3XHUwMDFivdOjeH3vry+bXHJcdTAwMTa1XHJrXHK+OrjoyZgoqMd+u9lcbpJTfTquJHgogFx0QGWAXHUwMDBiOzx7R2dBauVcdIbGXG5cdTAwMDCmmVx1MDAxOZ69XHLP4nN3XHUwMDA1cE9po4RcdTAwMTBMatAqueQ8XGKb57G7RlnPai0tSGFcclqjhtc8WPTnXG5cdTAwMWJcdTAwMWXpxVHnXCLY7LQ6kTP7t4BMXHUwMDEzkFx1MDAxOH3q1y+aUee6fTa8Jo78dq/rR9RLyXWNsNWqxneDu1NPU6+uZp5x/NhcdTAwMDSeOT7uW/TQ5nk76LmRgOHRTtevh/Ggs1jSXG5nYXfnbDBo/yY2Rf5lsONGrX3dalxyXHUwMDBmh+2zwI3Fqv9h5Gnts8enPY14Mpzi8ciPxPYgcDdcdTAwMDbDJUeQPLElcTvJbfboQac9cEFcdTAwMWEjrSRXOumNsFch34tcdTAwMDe3bfitXpCMgbNtK+uXad9cdTAwMWNxvTjoJ1x1MDAwM5Py3PP3nG++75gmbtjdu/j+u9m6vlhcdTAwMWRe9+P34ts+fLnSPN6+61x1MDAwN9Ft4/iicrCxKe8+7X5cdTAwMWR9ytPz/Sjq3Ja970H09X6zetJRO/dcdTAwMTfrQfVcdTAwMWX8YL9S7r6PvyXjfd0981x1MDAxZvpcdTAwMGa0QMNcciguXHUwMDE1XHUwMDBlz7fC9kXWXHUwMDE5Wp36RdLl71JcdTAwMDZncF/cezncj3T+XHUwMDAz5JX0LGeCM8mFYTiKeHJcdTAwMDJPW4XCXHUwMDE4XHUwMDAyvlx1MDAxNDnEXHUwMDFi5Vn6KKW1XHUwMDEwSlx1MDAxNFx1MDAwMZ6/XHUwMDAxfFxmwHeKXHUwMDAxPnL1XHUwMDEzkplcdTAwMDZuJMhcIiRcdTAwMGKrxiFZSWY5RzSzXHUwMDAweZLvMrRT+W7iis5cdTAwMDWp9d1OL25GQS81hp12XFxccu+d2amA5Y5u+5dhy3W6XHUwMDFhuc16K2y69q/WyeIgWk13Qlx1MDAxY1x1MDAxMnlcdTAwMGUvuFxmz87SZFenm/phO4h2yrBmJ1xum2Hbb9XGmu5fx51PQe/B+Di6XHUwMDBl0j1cdTAwMTN8XHUwMDE4cp/H1Vx1MDAwNFx1MDAxMH8yx42DSmVfXHUwMDFm2PggvJXrd4HeLU3eKNHjgvzEcoKizHA3XHUwMDAwesZwQb5gLWpIenJI3oieXHUwMDA2YYicmWLapC5cdTAwMTliWXGPKVRgKGyg0amHPFx1MDAwZm2wXHUwMDFh6vxcdTAwMTeB9l9zc7dw/EBdbNJe/Vx1MDAwNHhcdTAwMThL3VxiyIWcXHUwMDEx8LMx99bGfX2tq079L9Xji9pm52qvodplXHUwMDE5duuue9TcvmrWjs73m93Ozl6t395YXHUwMDE0w3JQmKKfuVx1MDAxOLa4lSVcdTAwMTiWXHUwMDAwR1x1MDAxY+pAJUhcdTAwMTFcdTAwMGIhM8BcdTAwMTTMk8CUUe5cdTAwMWFcdTAwMWHzXHUwMDAyVW1JVVvLlVx1MDAxMlx1MDAxYZhcdTAwMTXzkuwvhcTdKUhWXG7FXHUwMDE5N1hEstzqsZhcdTAwMTOMRk+luHlBJGsoXHUwMDBmk1O4b45kqzGdX9k896N4XHUwMDE5ePZcdTAwMTmCy/JsofWLodq1hj0/qVx1MDAxY/fhplx1MDAwN8dcdTAwMDfyslNpN//KozlotcJuL0O0UmpcdTAwMDK0tUZcbqaUsjIjmrlkXHUwMDFlKtJHXHUwMDAwxoLAJFZcdTAwMGZcdTAwMTFNmKfkXG5cdTAwMDXlwYBcdTAwMDT+PKAhNTTPI5o3bCDlL4LovbLcysdxq9KGXCKpXHUwMDE2PO3JjzhcdTAwMDfMMe4wLXZTXHUwMDFhUqN9xbT4XHUwMDEyb/j1afP+qnL9+fv2aXB4yLe+/uy0WG33w/fV25q4rjTC9e37XHUwMDFhyIpYXHUwMDE4aUsplU6jenbSLu69XHUwMDEypC219UgzW6FcdTAwMDWh3KpcZmlcdTAwMTN0ndZWUlx1MDAxMqdbKXNcYkftIVgrJDOUXFxcdTAwMDMvQLiehrJ/KYDvT0HZ2qKifMZcdTAwMTRRNqjc0ScoS0N5sZhccskjduRJ2/3nU7hvjrRrIZ2O/mlcdTAwMWaFwe0/7WpcdTAwMTDdhPVgXHUwMDE56PtcdTAwMTnSzNJ3ulx1MDAxZCu5ZiyGx1x1MDAwZnzY5r78Uls78bfX77e/NdhBgSpcdTAwMWaTMmujPaM1XG5lKPFcdTAwMDWTXGbbXHUwMDAzylx1MDAxOfdcZoFcdTAwMTc0Ssu5fsuZX1x1MDAxNPaN+ee7XHUwMDE59ZdmLE/hrqWI2aNP0YBcdTAwMDZaSyVcdTAwMTBekdg3zIdvO7WjzcruPutf1Wq3XHUwMDFm7/dcdTAwMWJlXHRY7lx1MDAxZF6dVFx1MDAwZdkmnt5/Qt6Fu+2IL4yAOfHaNGnHXHUwMDA0fFx1MDAxNreyXHUwMDA0XHUwMDAxXHUwMDFipj1gXHUwMDEyXHUwMDA1ZUCcodZcdTAwMTloXHUwMDAyeoKkt1x1MDAxM2OoNFx1MDAxNGTN3FDosMZcIpeG0mcm8tB8y5rHYbFZnoKtRlxujdZcdTAwMTRiTo3FXHUwMDFjSCs5RyNwXHUwMDE2zE3Mmlx1MDAwNcm2eVxiePM6ckO48jFaXHUwMDEy4n2G5bLEO8b+xTBu7729ONyWXHUwMDA3XHUwMDFm11x1MDAxYaf25rK53o9OVGnGJVx1MDAxMvSAaNdKplxilzxx31x1MDAwN1hb7XFpXHUwMDExUStELlxulpjfXHUwMDE4d3EoP5+bcYlsheY0lEXgt7mjqUx6oLdwpimz2Vxi94vZVerk61xy1+v3sX/VaWB/66+yhHtcdTAwMTls1Pb6ZztqZ+f9wdXJVv/jbny4MMJcdTAwMTUkMVx1MDAxNzRNXdzKMoSriS+10pqUriHUZFxiV1x1MDAwMPO0O4lMXGJKuDCPTM48RTkv10KR1kJTgMw3wlx1MDAxZFx1MDAwN8VwmmlqaVxmoIGi6Ss+fi3YwY1G+Fx1MDAwNaap0bK5pqlcdTAwMGa7QXvlMDoje5eBbp+huCzdXHUwMDE2Wr9cdTAwMTiyhcr3XG73d8+gft31zdbO1sXe/6qlyZaEvIeCW+titCRsZieqOVx1MDAxMaVBVIpbYXS+uuONbFx1MDAxN4jw7/Ont1x1MDAxMrlcdTAwMGLQWMS2Qowv50JGUDUsXHUwMDA10lx1MDAxN2dbqLJcdTAwMGbbW13daPaC9v5R43PU/7S5XHUwMDFj88vETjDNXHUwMDA03SR8XHUwMDE2trJcdTAwMDTbcik9JixlTW5ZmFx1MDAwNig79SRcYrrgiiRdcptek0jo1nrGXHUwMDEytrmQQr9x7Vx1MDAxNEi8mIJriU+lXHUwMDE0Qlx1MDAxN3GtXHUwMDEwY+eXgYItXHKbNYsm26m9N0e2XHUwMDBm07IrVTKmvlx1MDAxNKvCz5BcXPG0cs7+xVx1MDAxMO7n+mWvWf34rXd5tenf3PV2P1x1MDAxZfKTkuvCNC5cdTAwMWVqUvPMXHUwMDE1emhTsCzMQGhGrqGMYlx1MDAwNbPJgtFccqQrJeAo6Eeiwd/WhZ+HdasswY5dXHUwMDE3XHUwMDA2SaNmqOtzs1buW3m9naSzpJFcdTAwMTDUbGifjWA3N77Xalx1MDAxZqr7l+KkWd+qVI/ws/78XG51zVx1MDAxM+87RzXXxPvOMd898b4vnNYrUFx1MDAwYkrri0e7hNBAJzSYXHUwMDAycm1cdTAwMTJcdTAwMTUyt8SF3FNGXHUwMDEzyzErUVx1MDAxNSxxva1kr8xcdTAwMWWSLqdQXHUwMDFhglx1MDAxYmmIXHUwMDAyXG5XssdPpVkyXHUwMDEzSSgufFx1MDAxYVx1MDAxZM2cWf3SrmM/Q/I/Y1x1MDAxZHtypU4mXGKNXHUwMDAwnHJ3XHUwMDBmpZBIwoJcdTAwMTJcbpGZUVx1MDAxN267lVHGkiqhXHUwMDE0Pj9tJ7SnmWBcXGuUXG5cdTAwMDGKNFx1MDAwN4Bn3JRcdTAwMTNlLaSGp8orXHUwMDAy9+9cdTAwMTdBe3tuXHUwMDAxwrVcdTAwMDHK+VhRgq+5yFx1MDAxZVx1MDAxZOpcdTAwMGbralx0LVOvuH5cdTAwMWTL9/3K+je28f3zpzN5vlx1MDAxN7StmbbQXHUwMDBipbFpuJSNMN1OmLX975SFLG0uXHUwMDFi/v7v74VXj1x1MDAwNYD7ZF0/uVmufS2/XHUwMDE3b3YuL8OYWvnRWZiL9LFcdTAwMWbFXHUwMDFi5C9huznqXHUwMDE3j9s6y5TZXGZiZP3atZ7yXHTFONG3MFx1MDAwMIxzt60ydVnT7z5cdTAwMDZcdTAwMWUlrWaGuJtcdTAwMTHVm/Sdwv6orTnHXGbaZ89bPHn/TMriNTKZtIZl1KGorKDolJRgXHJNXHUwMDE2niQyXHUwMDE0XHUwMDFhXHUwMDA1XHUwMDAz0uNW6alMXHUwMDFldPO6i5TngZ+DKzUofS5cdTAwMWJSg9Zp57aUJCtcdTAwMDZAXHRJpihP1EYrzlEylWrd4y5bRkNGXHUwMDAz5VZRpDSYnFx1MDAxZW66c0sx1lx1MDAxYVx1MDAxMm9cdTAwMTT5XHIk0WLGyZ9fKUh3ppBkiiS826RcXCTJJlRcdLttXHUwMDFhQqlZXHUwMDE02TO1hVx1MDAxNKD4XFya7OCwtrN9slxmXCLsXHUwMDE5xZNcdTAwMTVhWcNcdTAwMTdUPjgxXHUwMDBmniS7UHBcIlx1MDAwNlx03FJI5ZLltspLXHUwMDBmJP2kkEuZU0GFsGSe1cZKgYBCIGJcdTAwMDGKgWmPWy4o82LGUpBPcPSG6lx1MDAxMVR355ZeSlx1MDAwYq6guIzJ8LGLqqA0WmRMvuLaStTyb6JcdTAwMGbrolI/2q00bk9cdTAwMTHU4fG0U1x1MDAxM1rONIu8UOk1XHUwMDFlXHUwMDAz7rOWd//XkF+lxVxmaVx1MDAxOamcrpKGXHUwMDExaVx1MDAwMzNcInXVk/pcIqXjLuCunIlcdTAwMTh9OilTSn1NTmdH1ZdwXG7Qlcm6mVx1MDAxY2Pzclx1MDAxMT0rLVx1MDAwN2GEXHUwMDAwYyzIqex9JelVXGaAXHUwMDEy0lx1MDAwYkF6XHUwMDAyyN2se9dcdTAwMDHL7tyS5HCkq0gvM00qtaD8TFx1MDAxYlx1MDAwZsBaylx1MDAxNOjHXCK2Yv5KQfqqvPRcIt1cdTAwMGKUXHUwMDFkXHUwMDE01pWN3/wsKJHTs1aVTZpcdTAwMGKzkC6emEF37e1Ua1tcdTAwMDfLoLuekTxZ3ZU1fDG6a/I6wSTdpZmraVx1MDAwMVdcdTAwMDFFfJ2raCFGIVx1MDAxNCuGdE4qlVx1MDAwNzA3nuZcdTAwMTLQUsKMXHUwMDEylCqQXZpRXHUwMDAyXHUwMDA2iOTyilk0SVx1MDAwNvZcdTAwMDboXHUwMDExQEdzqy5giv5rKFxcX7d8wo5ccvJcdTAwMDFcIjCz6Fx1MDAxN5ssiTJcdTAwMWHvpu6zlvPQ11x1MDAxMEaldYZcdTAwMTNGg/loJklcdTAwMWNcdTAwMTGRJmXeQ52hXFzlXHUwMDE5RzctLThpQEzfZzG6aPJu81FdXHUwMDA0biNcdTAwMDJzO1x0mXtdli5QcssohCavi06Mo5KUXHUwMDFluNc2kYel64tcdTAwMWXiqHZcdTAwMWLjXHUwMDE0kvNcdTAwMTlFXG42v71GWM9cItBcdTAwMTBqXHUwMDAwVFxmUrNcdTAwMWQpJeQxziz1JoDRwN/i6Jg4XHUwMDFhz1x1MDAxZEdpXGJcdTAwMTDcdpqiMKrHvrnCXHUwMDExppO7L1x1MDAxMkWT5YqfNrU/1kndZy3nn0tcdTAwMTZFuVx1MDAxObyZzS3OXHUwMDAzXHJtssw+XGZK2nPvjaEmSm40NXK6IFUqik7eezRiL1xyvdRcdTAwMTa0JUuMMpgzdylj6ORcdTAwMWGQyTFUufTeMnR5XHUwMDA3ZF+eJ4D8S1x1MDAxMsKkkdZcdTAwMTFdLohSXCLukVdcdTAwMWGjhXTjJ1x1MDAwYoIoPcLQXHUwMDEzmFveY8K8rb6OXHUwMDBiotfzi1FBiT2zxe/cgvRsQW79lcbmxdToz4+j4/3UfXJcdTAwMWW6ZGGUUncrpSbBXGaGQ6qoZSVZcNRKU1x1MDAwYlBcdTAwMWLBXFxB9Vx1MDAwYszSTd5UMqpGXHUwMDA3Qd9cdTAwMTItS2nSL+VcXO5AOrlqf1IgXHUwMDE1VnhCojGGaa1cdTAwMDSD7OtWlCtic5PA4NK+grReU6hFcj5yQJTp7b/JWoqnKFx1MDAxM9GcKzV480dyj7c4Olx1MDAxMkdv5o6jRoKl5LSwkI2bXHRVLCRmUNuZXHUwMDE2Tl+mZn6hUXScj7pPzjtfI4ZOLrxcdTAwMWaNoe513lx1MDAxMtx77oTVKl0kM1NIKlx1MDAxNTJLXHUwMDE3wlDI5Mq9YNG6vWooMD/f8LMj5rvHXHUwMDExXFz1u91qTP45bPfqTVx1MDAxONxu5CPKb43Bx63rXHJcZnPRJFx1MDAxOCDxx7tcdTAwMWb/XHUwMDA3dIVcdTAwMTKqIn0=postgresStock ChartTickerViewServiceCurrent PriceOpen OrdersTicker SelectTickerViewServiceNOTIFYLISTEN

Ticker View Schema

CREATE TABLE ticker_view
(
    id        SERIAL PRIMARY KEY,
    username  TEXT NOT NULL,
    ticker    TEXT NOT NULL
)

Basic Ticker View Pub Sub

We’ve seen all this before…

INSERT INTO ticker_view (username, ticker)
VALUES ('paul', 'AAPL')
RETURNING id
SELECT pg_notify('ticker_view', '1')
LISTEN ticker_view;
SELECT ticker FROM ticker_view
WHERE id = 1
ticker
AAPL

Goal: High Availabilitiy

  • What if the UI briefly loses connection to the service?
  • What if the service briefly loses connection to the database?
  • ❗ Gotcha ❗: You miss messages when you aren’t connected

Querying for what we missed

  • How do we know what we missed?
  • Let’s assume the UI was keeping track of the ID of the last message it received
    • In this case, 0 to indicate it hasn’t received any messages
SELECT ticker FROM ticker_view
WHERE id > 0
AND username = 'paul'
ticker
AAPL

Multiple missed messages, like Kafka

But what if we missed multiple messages?

SELECT id, ticker FROM ticker_view
WHERE id > 0
AND username = 'paul'
id ticker
1 AAPL
2 NVDA
  • Querying since an ID is analogous to a concept in Kafka called consumer groups
  • Consumer Groups help you pick up where you left off, by keeping track of the last message ID (“offset”)
  • With Kafka you have to consume every message, which we don’t want to do in this case

Multiple missed messages, with Postgres

For this use case, we only care about the last message of the ones that we missed

SELECT id, ticker FROM ticker_view
WHERE username = 'paul'
AND id > 0
ORDER BY id DESC LIMIT 1;
id ticker
2 NVDA
  • This is more useful to us than Kafka Consumer Groups!
  • Not having to read every message is an advantage
  • It all depends on the use case
  • 🟢 Pattern 🟢: LISTENer doesn’t need to care about every message

Patterns and Gotchas Summary

Patterns and Gotchas

  • ❗ Gotcha ❗: NOTIFY payloads have a hard 8kb limit
  • 🟢 Pattern 🟢: NOTIFY instructions about what to read, not the data itself
  • 🟢 Pattern 🟢: LISTENer doesn’t need to care about every message
  • 🟢 Pattern 🟢: On LISTEN, query for the whole state, let the database manage the state
  • ❗ Gotcha ❗: The LISTEN thread can get backed up
  • 🟢 Pattern 🟢: Do your work elsewhere from the LISTEN thread

Some notes on scale, Kafka, and Postgres

  • Kafka is good for lots of messages and lots of subscribers
  • Do you really need to send that many messages?
  • Do you really need that many subscribers?
  • You can get far on Postgres using the right patterns

Thanks For Listening!

Questions?

  • Presentation: pgwhalen.com/pg-pubsub-talk
  • Source: github.com/pgwhalen/pg-pubsub-talk/