Skip to content

← Raw Messaging Reliability | Message API and ownership →

Design Rationale — Why It Was Built This Way

What this chapter answers — it explains the reasoning behind zlink's core design decisions from a user's perspective. Implementation detail is owned by internals.

This document explains, from a user's perspective, the reasoning behind the core design decisions zlink adopted. Implementation detail is owned by internals; this document instead focuses on "what this choice means for the user." It's written for a reader evaluating whether to adopt zlink or trying to understand its performance characteristics.

Starting Point — What Changed From libzmq

zlink starts from libzmq v4.3.5 and is a library that focuses on the core patterns and narrows the surface. See the overview for the detailed comparison table. The gist is:

  • 17 socket types → 8 (PAIR, PUB/SUB, XPUB/XSUB, DEALER/ROUTER, STREAM).
  • The I/O engine went from a self-built poll/epoll/kqueue to Boost.Asio (bundled, no external dependency).
  • Encryption went from CURVE (libsodium) to TLS (OpenSSL).
  • The dependency footprint narrowed to just OpenSSL.

The reason for narrowing is simple. Exposing less makes it easier to polish each pattern deeply and keep it consistent, and gives the user fewer choices, which also means less room to choose wrong.

Core Design Principles

Zero-Copy — Small Messages Inline, Large Messages Reference-Counted

A small message (29 bytes or less) is stored directly inside the message object without a separate heap allocation (VSM, Very Small Message). A message larger than that is shared without copying, through reference counting.

What this means for the user: in a workload with many small control messages (ticks, heartbeats, short commands), allocation and copy cost disappears. The move semantics where a send "consumes" the message also come from this model — copy explicitly when you need to keep it (09 Message API).

VSM is only a memory optimization — it has no effect on the wire format. The receiving side never needs to know whether the sending side used inline storage (ZMP reference).

Lock-Free — YPipe For Inter-Thread Communication

Message delivery between threads uses a CAS (Compare-And-Swap)-based FIFO queue (YPipe) instead of a lock.

What this means for the user: there's no lock contention on the hot path, so multicore scaling works well. The public socket handle API is thread-safe: several threads may send on the same socket concurrently, while the receiving side of one socket is kept to a single consumer thread (11 Thread Safety).

True Async — The Proactor Pattern

Built on Boost.Asio, I/O completion events are delivered to a handler (Proactor). This is a structure that gets notified of completion rather than polling I/O directly.

What this means for the user: I/O completions are handled inside the I/O threads the Context owns, and user code never runs there — Core has no application callbacks; the application waits for readiness with a poller and then pulls results with a socket-specific whole-message receive function or zlink_completion_recv(). Multiple sockets are grouped under one loop with a poller (the concept is in 02 Core API; the per-language surface is in each binding guide).

Protocol Agnostic — Separating Transport From Protocol

The wire protocol (ZMP) and the transport (tcp/ipc/inproc/ws/tls) are clearly separated.

What this means for the user: the same messaging code moves from inproc (same process) to tcp (network) by changing only the transport — just the address scheme changes. For tls/wss, though, beyond the address scheme, you also need to configure zlink_set_tls_server() (server cert/key) and, if needed, zlink_set_tls_client() (04 Transport, 05 TLS/Security).

Going Deeper