jonnifyechomq
EchoMQ / Overview / The three pillars

Overview · dive 1 · what

The three pillars

One wire, three surfaces. The Queue distributes work, the Bus broadcasts signals, the Cache serves reads.

EchoMQ is one Valkey-native job system you own, canonical in Elixir. Above the one wire it owns stand three pillars, and each answers a different question about moving data between the parts of a system: the Queue hands one job to one worker, the Bus announces one fact to many listeners, and the Cache keeps one hot value close. This dive names the three surfaces and the real function that opens each one; the deep mechanism is the pillar chapters on the course map. Pick a pillar on the right to read its job and its shape.

Three pillars over one wire · pick a pillar

The Queue distribute work one job one worker point to point The Bus broadcast signals one event many listeners one to many The Cache serve reads one value read often read through one owned wire the keyspace + the atomic transitions
The Queue — distribute work. Shape: point to point — one job, one worker. Surface: EchoMQ.Jobs.enqueue/4 + claim/3, EchoMQ.Consumer.

Three surfaces, one wire

EchoMQ owns one Valkey keyspace and one set of atomic transitions, which run as Lua scripts and, for the core job transitions, as native commands. Above that line sit three product surfaces, and the difference between them is the question each one answers. The Queue distributes work: a task is handed to exactly one worker, runs once, and is marked done. The Bus broadcasts signals: a fact is announced, and every interested listener hears it. The Cache serves reads: a hot value is held close, so a read does not pay the full cost twice. Three surfaces, three jobs — one substrate underneath.

Each surface is a real module. The point here is which function is the door into each pillar; the mechanism behind the door is taught in depth in the pillar chapters.

The Queue — distribute work

A job is admitted with EchoMQ.Jobs.enqueue/4, claimed by a worker with EchoMQ.Jobs.claim/3, and the loop that owns the claim rhythm is EchoMQ.Consumer. Admission is one atomic step on the server: the Elixir verb checks the queue name, passes the job key and the pending set as the two declared keys, and the script does the rest.

# echo/apps/echo_mq — EchoMQ.Jobs.enqueue/4
# Admit a job in one atomic step. enqueue/4 is this function with opts = [].
# The verb passes the job key and the pending set as the two declared KEYS;
# the named @enqueue script does the rest, on the executor `via` names.
def enqueue(conn, queue, job_id, payload, opts \\ [])
    when is_binary(job_id) and is_binary(payload) do
  # a brace or NUL in the name raises
  Keyspace.valid_queue!(queue)
  # EchoMQ.Connector by default: EVALSHA first
  via = Keyword.get(opts, :via, Connector)
  keys = [Keyspace.job_key(queue, job_id), Keyspace.queue_key(queue, "pending")]

  case via.eval(conn, @enqueue, keys, [job_id, payload]) do
    # admitted
    {:ok, 1} -> {:ok, :enqueued}
    # already present — idempotent
    {:ok, 0} -> {:ok, :duplicate}
    # not a JOB-namespaced id
    {:error, {:server, "EMQKIND" <> _}} -> {:error, :kind}
    other -> other
  end
end

The admission rule itself lives in one named Lua handle — EchoMQ.Jobs @enqueue — run on the server so the kind check, the duplicate refusal, the row write, and the pending insertion happen as one indivisible step.

-- EchoMQ.Jobs @enqueue — the atomic admission script
-- KEYS[1] = the job key   KEYS[2] = the pending set   ARGV[1] = id   ARGV[2] = payload
-- the kind gate: the id's first three bytes are JOB
if string.sub(ARGV[1], 1, 3) ~= 'JOB' then
  return redis.error_reply('EMQKIND job id must be JOB-namespaced')
end
-- already admitted? refuse, idempotently
if redis.call('EXISTS', KEYS[1]) == 1 then
  return 0
end
-- write the row
redis.call('HSET', KEYS[1], 'state', 'pending', 'attempts', '0', 'payload', ARGV[2])
-- same score: byte order is mint order
redis.call('ZADD', KEYS[2], 0, ARGV[1])
return 1

The same admission is also one native command. On a node that loads the native module — trafikey, a Valkey module loaded at start on its own node — the transition is EMQ.ENQUEUE 2 emq:{q}:job:<id> emq:{q}:pending <id> <payload>: the same two keys, the same id and payload, and the same replies. The Elixir verb above runs the script; the native command is the same transition compiled into the server, for any client that sends it. Both executors are taught in The Protocol.

# the same admission, one native command (a node that loads the native module)
EMQ.ENQUEUE 2 emq:{q}:job:<id> emq:{q}:pending <id> <payload>
#  1        admitted: the row and the pending entry, written in one step
#  0        a duplicate: the row exists, nothing is written
#  EMQKIND  refused: the id is not JOB-namespaced

The Bus — broadcast signals

A listener subscribes with EchoMQ.Events.subscribe/2; a lifecycle fact is announced with EchoMQ.Events.publish/5 on the per-queue channel EchoMQ.Events.channel/1 (emq:{q}:events). One event reaches every subscribed listener — the broadcast shape. Beyond the live broadcast, the Bus keeps a retained, replayable event log: append order is mint order, a reader can start at an offset and read forward, and a consumer group recovers what it missed.

# echo/apps/echo_mq — EchoMQ.Events.publish/5
# Announce a lifecycle fact to every listener on the per-queue channel.
def publish(conn, queue, event, job_id, extra \\ []) do
  # gate the id at the key builder before the wire
  _ = Keyspace.job_key(queue, job_id)
  payload = encode_event(event, job_id, extra)

  case Connector.command(conn, ["PUBLISH", channel(queue), payload]) do
    # delivered to live subscribers
    {:ok, _n} -> :ok
    other -> other
  end
end

The Bus has two faces: the live pub/sub broadcast above, and a retained event log a consumer can replay from an offset. The event log — mint-ordered append, read-at-offset, consumer-group recovery — is taught in depth in the Bus pillar chapter.

The Cache — serve reads

A hot value is read with EchoStore.Table.fetch/3: a hit returns from L1 (an ETS table) in the caller's own process, and a miss falls through to a single fill that checks L2 (Valkey), runs the declared loader, and writes both layers. The read path is the cache's whole job — one value, read often, held close.

# echo/apps/echo_store — EchoStore.Table.fetch/3
# Read through the cache: an L1 hit never enters the owner process;
# a miss is a single-flight fill through the owner (L2, then the loader).
def fetch(name, id, timeout \\ 10_000) do
  case EchoStore.spec(name) do
    :error ->
      {:error, :no_such_cache}

    {:ok, spec} ->
      # wrong-namespace id refused at the door
      with :ok <- gate(spec.kind, id) do
        now = System.monotonic_time(:millisecond)

        case :ets.lookup(name, id) do
          [{^id, value, expires_at, _version}] when now < expires_at ->
            # count the hit
            :counters.add(spec.counters, @counters[:hits], 1)
            # L1 hit, in the caller's process
            {:ok, value, :hit}

          _ ->
            # count the miss
            :counters.add(spec.counters, @counters[:misses], 1)
            # single-flight fill: :l2 or :fill
            GenServer.call(name, {:fill, id}, timeout)
        end
      end
  end
end

Three messaging shapes

The three pillars are three classic messaging shapes, and naming the shape is the fastest way to know which pillar a need belongs to. A queue is point to point: a message goes to one consumer, and a second consumer competing on the same queue gets a different message, never a copy. A bus is one to many: a message goes to every subscriber, each getting its own copy. A cache is neither — it is a read path: the same value is served to whoever asks, again and again, until it expires.

The Queue
Point to point. One job is claimed by exactly one worker. Competing workers share the load; no worker sees another's job.
The Bus
One to many. One event is delivered to every subscriber, each with its own copy. A new subscriber changes nothing for the others.
The Cache
Read through. One value is served to every reader until it expires. A miss fills once; the herd reads the single answer.

Name the shape, and the pillar names itself. One job to one worker is the Queue; one fact to many listeners is the Bus; one value read often is the Cache.

the pattern (redis-patterns)

The Redis pattern catalog teaches each shape on its own — reliable queues for work distribution, streams and events for broadcast and replay, the cache-aside family for the read path. Three patterns, taught separately.

the pillars (EchoMQ)

EchoMQ applies all three over one owned keyspace and one set of atomic transitions — EchoMQ.Jobs.enqueue/4 + claim/3, EchoMQ.Events.subscribe/2 + publish/5, and EchoStore.Table.fetch/3. Three surfaces, one substrate.

Which pillar fits the need

Match a real need to its pillar. Pick a need below; the readout names the pillar, the shape it fits, and the surface that does it. The rule is the shape: does the data go to one worker, to many listeners, or back to readers until it goes stale?

A need · which pillar fits it

The Queue fits. Shape: point to point — one job, one worker. Surface: EchoMQ.Jobs.enqueue/4 → claim/3. Why: the work must run once, on exactly one consumer.

Some needs ride more than one pillar — an order that settles is broadcast on the Bus and may invalidate a cached read. The Overview names the surfaces; the pillar chapters show how they compose. Next: the protocol below the line that lets them share one wire.

Recap — the three pillars

EchoMQ is one system with three surfaces over one owned wire. The Queue distributes work point to point (EchoMQ.Jobs.enqueue/4, claim/3, EchoMQ.Consumer). The Bus broadcasts signals one to many (EchoMQ.Events.subscribe/2, publish/5) and keeps a replayable log. The Cache serves reads through two layers (EchoStore.Table.fetch/3). Each is named here and taught in depth in its own pillar chapter. The next dive draws the line that lets all three share one wire — the protocol below the language line.

Next in the Overview: The protocol below the line — why the three pillars interoperate, and why any runtime can speak them.

References

Where the three pillars are grounded, and where the engine and the dispatch they share are documented.

Sources

  • Valkey — Documentation — the BSD-licensed, foundation-governed store EchoMQ is backed by — the substrate of record for all three pillars.
  • Redis — EVALSHA — the load-once, run-by-SHA dispatch the scripted executor runs each atomic script through.
  • Redis — Documentation — the data structures the keyspace under the three pillars is built from.
  • Valkey — Cluster specification — the {hashtag}→hash-slot routing the per-queue-hashtag keyspace is built for: every key of a queue on one slot, where a multi-key script is legal.

Related in this course