Files
freemo d9e5668cec fix(skills): comprehensive final audit pass for programming-patterns skill
Fixes and improvements from exhaustive audit:

Consistency fixes in SKILL.md:
- 'Pipe & Filter' → 'Pipe and Filter' (one stray '&' found and corrected)
- 'Singleton for factory instance' → clarified to 'register factory as
  singleton-scoped via DI container' (less misleading wording)
- Documentation Format section updated with note that SKILL.md itself is the
  authoritative source for related-pattern combinations

Coverage fix — Related Patterns sections:
- Added '## Related Patterns' to ALL 94 pattern files (was 0/94)
- Each section lists 3–6 related patterns with relationship descriptions
- Covers: why they're related, when to prefer one vs the other,
  and which are often confused

SOLID principles → Creational → Structural → Behavioral → Architectural →
Concurrency → Functional → Resilience → Data Access → Messaging →
Testing → Error Handling → Microservice — all 13 categories covered

Code verification:
- Python: 0 failures (all 85 testable blocks pass)
- Go: 0 failures (all 76 testable blocks pass)
- JavaScript: 0 failures (all 78 testable blocks pass)
- All 239 code blocks verified correct after edits

Final skill state:
- 108 files, 36,524 lines across 13 reference categories
- 94/94 pattern files have Related Patterns sections
- 2,815-line SKILL.md with 67 decision trees, 23 scenarios,
  0 broken references, 0 naming inconsistencies
2026-04-15 13:22:13 -04:00

8.1 KiB

Request-Reply Pattern

Problem

Standard message queues are fire-and-forget — the sender publishes a message and moves on. But sometimes the sender needs a response: a query result, a validation outcome, or a computed value. Without a structured way to correlate requests with responses, implementing RPC over messaging becomes ad-hoc and error-prone.

Solution

The sender includes a correlation ID and a reply-to address (queue/callback) with each request. The receiver processes the request and sends the response back to the specified reply address, including the same correlation ID. The sender matches responses to their original requests using the correlation ID.

When to Use

  • RPC-style communication over message infrastructure
  • When you want the decoupling of messaging but need responses
  • Async queries across services
  • When the response might take significant time (long-running computations)
  • Gateway patterns that aggregate responses from multiple services

When to Avoid

  • Simple fire-and-forget work (use plain message queue)
  • When low latency is critical (direct HTTP/gRPC is more efficient)
  • When the response is not needed
  • High-throughput scenarios where correlation tracking becomes expensive

Pseudocode

# Sender
function send_request(payload):
    correlation_id = generate_uuid()
    pending[correlation_id] = create_future()
    send(message={payload, correlation_id, reply_to=my_reply_queue})
    return pending[correlation_id].await(timeout)

# Receiver
function on_message(message):
    result = process(message.payload)
    send(reply_to=message.reply_to,
         message={result, correlation_id=message.correlation_id})

Python Implementation

import uuid
import threading
import time
from dataclasses import dataclass, field
from typing import Any, Optional
from queue import Queue, Empty


@dataclass
class Message:
    payload: Any
    correlation_id: str = field(default_factory=lambda: str(uuid.uuid4())[:8])
    reply_to: Optional[str] = None


class MessageBroker:
    """Simple in-memory broker with named queues."""

    def __init__(self):
        self._queues: dict[str, Queue] = {}

    def get_queue(self, name: str) -> Queue:
        if name not in self._queues:
            self._queues[name] = Queue()
        return self._queues[name]

    def send(self, queue_name: str, message: Message):
        self.get_queue(queue_name).put(message)

    def receive(self, queue_name: str, timeout: float = 2.0) -> Optional[Message]:
        try:
            return self.get_queue(queue_name).get(timeout=timeout)
        except Empty:
            return None


class RequestReplyClient:
    def __init__(self, broker: MessageBroker, reply_queue: str):
        self.broker = broker
        self.reply_queue = reply_queue
        self._pending: dict[str, threading.Event] = {}
        self._responses: dict[str, Any] = {}

        # Start reply listener
        self._running = True
        self._listener = threading.Thread(target=self._listen, daemon=True)
        self._listener.start()

    def _listen(self):
        while self._running:
            msg = self.broker.receive(self.reply_queue, timeout=0.5)
            if msg and msg.correlation_id in self._pending:
                self._responses[msg.correlation_id] = msg.payload
                self._pending[msg.correlation_id].set()

    def request(self, target_queue: str, payload: Any, timeout: float = 5.0) -> Any:
        corr_id = str(uuid.uuid4())[:8]
        event = threading.Event()
        self._pending[corr_id] = event

        msg = Message(payload=payload, correlation_id=corr_id, reply_to=self.reply_queue)
        print(f"  [Client] Sending request (corr={corr_id}): {payload}")
        self.broker.send(target_queue, msg)

        if not event.wait(timeout=timeout):
            raise TimeoutError(f"No reply for correlation {corr_id}")

        response = self._responses.pop(corr_id)
        del self._pending[corr_id]
        return response

    def stop(self):
        self._running = False


class RequestReplyServer:
    def __init__(self, broker: MessageBroker, listen_queue: str, handler):
        self.broker = broker
        self.listen_queue = listen_queue
        self.handler = handler
        self._running = True
        self._thread = threading.Thread(target=self._process, daemon=True)
        self._thread.start()

    def _process(self):
        while self._running:
            msg = self.broker.receive(self.listen_queue, timeout=0.5)
            if msg is None:
                continue
            print(f"  [Server] Received request (corr={msg.correlation_id}): {msg.payload}")
            result = self.handler(msg.payload)
            reply = Message(payload=result, correlation_id=msg.correlation_id)
            print(f"  [Server] Sending reply (corr={msg.correlation_id}): {result}")
            self.broker.send(msg.reply_to, reply)

    def stop(self):
        self._running = False


# --- Demo ---
broker = MessageBroker()

# Server: computes square of numbers
def compute_handler(payload):
    time.sleep(0.1)  # Simulate work
    return {"input": payload["value"], "result": payload["value"] ** 2}

server = RequestReplyServer(broker, "compute_queue", compute_handler)
client = RequestReplyClient(broker, "client_reply_queue")

print("=== Request-Reply Demo ===")
for val in [5, 12, 7]:
    response = client.request("compute_queue", {"value": val})
    print(f"  [Client] Got reply: {val}^2 = {response['result']}")
    print()

client.stop()
server.stop()
print("Done.")

Output:

=== Request-Reply Demo ===
  [Client] Sending request (corr=abc12345): {'value': 5}
  [Server] Received request (corr=abc12345): {'value': 5}
  [Server] Sending reply (corr=abc12345): {'input': 5, 'result': 25}
  [Client] Got reply: 5^2 = 25

  [Client] Sending request (corr=def67890): {'value': 12}
  [Server] Received request (corr=def67890): {'value': 12}
  [Server] Sending reply (corr=def67890): {'input': 12, 'result': 144}
  [Client] Got reply: 12^2 = 144

  [Client] Sending request (corr=ghi24680): {'value': 7}
  [Server] Received request (corr=ghi24680): {'value': 7}
  [Server] Sending reply (corr=ghi24680): {'input': 7, 'result': 49}
  [Client] Got reply: 7^2 = 49

Done.

Go Implementation

package main

import (
	"fmt"
	"sync"
	"time"
)

type Message struct {
	Payload       interface{}
	CorrelationID string
	ReplyTo       string
}

type Broker struct {
	mu     sync.Mutex
	queues map[string]chan Message
}

func NewBroker() *Broker {
	return &Broker{queues: make(map[string]chan Message)}
}

func (b *Broker) Queue(name string) chan Message {
	b.mu.Lock()
	defer b.mu.Unlock()
	if _, ok := b.queues[name]; !ok {
		b.queues[name] = make(chan Message, 100)
	}
	return b.queues[name]
}

func main() {
	broker := NewBroker()
	requestQ := broker.Queue("compute")
	replyQ := broker.Queue("replies")
	done := make(chan struct{})

	// Server goroutine
	go func() {
		for {
			select {
			case msg := <-requestQ:
				val := msg.Payload.(int)
				fmt.Printf("  [Server] Received (corr=%s): %d\n", msg.CorrelationID, val)
				time.Sleep(50 * time.Millisecond)
				result := val * val
				fmt.Printf("  [Server] Replying (corr=%s): %d\n", msg.CorrelationID, result)
				broker.Queue(msg.ReplyTo) <- Message{
					Payload:       result,
					CorrelationID: msg.CorrelationID,
				}
			case <-done:
				return
			}
		}
	}()

	// Client sends requests
	fmt.Println("=== Request-Reply Demo ===")
	for i, val := range []int{5, 12, 7} {
		corrID := fmt.Sprintf("req-%d", i+1)
		fmt.Printf("  [Client] Sending (corr=%s): %d\n", corrID, val)
		requestQ <- Message{Payload: val, CorrelationID: corrID, ReplyTo: "replies"}

		// Wait for matching reply
		reply := <-replyQ
		fmt.Printf("  [Client] Got reply: %d^2 = %v\n\n", val, reply.Payload)
	}

	close(done)
	fmt.Println("Done.")
}
  • Future/Promise — async request-reply is naturally modelled as a future: send the request, await the promise for the reply.
  • Message Queue — request-reply is built on top of message queues; the correlation ID links request to reply.
  • Timeout — always apply a timeout to request-reply so a lost reply doesn't block the caller indefinitely.