| ORM model + callbacks/signals | Ecto schema + context function; callbacks become explicit Ecto.Multi steps | side effects hidden in changesets |
| Sidekiq, Resque, Celery, BullMQ, Hangfire | Oban worker, inserted transactionally | Task.start, GenServer queue, Redis list |
| Cron (whenever, beat, node-cron, Quartz) | Oban top-level cron: (Oban.Cron, 2.24+) | :timer.send_interval loops |
| Action Cable, Socket.IO, SignalR, Redis pub/sub | Phoenix.PubSub + Channels or LiveView | re-implemented legacy socket framing (unless SHIM) |
| Redis cache, memoization | ETS or Cachex with complete keys, errors never cached | Redis kept "because it was there" |
| Redis rate limits, locks | Hammer; unique constraints; advisory or row locks every contender honors | SETNX re-implementations; Oban unique as a lock (it only dedupes inserts) |
Request globals (Current, thread-locals, AsyncLocalStorage) | explicit scope argument (Phoenix 1.8 scopes) | process dictionary |
Middleware, before_action, filters, guards | Plug pipelines, on_mount hooks | per-action copy-pasted checks |
| Service objects, managers, repositories | context functions over Repo | Service.call/1 modules everywhere |
| HTTP clients (Faraday, axios, requests, OkHttp) | Req behind a project-owned module | client chosen by habit |
| Mailers | Swoosh, delivered from an Oban job | synchronous send in the request |
| Hotwire, Livewire, HTMX, server views + JS | LiveView when the UI contract may change | hand-written Turbo protocol without coexistence need |
| SPA + REST/GraphQL | keep the SPA on Phoenix JSON or Absinthe; LiveView only if redesign is approved | rewriting the frontend inside the backend port |
| App-server pools (Puma, Gunicorn, PM2, thread pools) | DELETE — BEAM schedulers and Task.async_stream/3 | pools sized per request |