Skip to content

Communication Patterns: From Tight to Loose

Software systems communicate across a spectrum. At one end, modules call each other inside the same process: extremely fast, but tightly coupled. At the other end, systems exchange files or batches hours later: slower, but far more independent and resilient.

The general rule:

Tighter coupling = faster and simpler, but less flexible.
Looser coupling = slower and more operationally complex, but more resilient.

TIGHT / FAST LOOSE / SLOW
ns ─────────── μs ─────────── ms ─────────── s ─────────── hours
│ │ │ │ │
[1][2][3] [4] [5] [6] [7]
in-process same-host network sync network async batch/file

This spectrum is not a ranking from good to bad. It is a set of trade-offs. A well-designed system often uses several of these patterns at once.


[1] Direct Function / Method Calls — Nanoseconds

Section titled “[1] Direct Function / Method Calls — Nanoseconds”

Same binary, same address space, compiler or interpreter links them together. In compiled languages, the compiler can sometimes inline the call away entirely.

┌────────────── Process ──────────────┐
│ │
│ Module A ─────── call ───────▶ Module B
│ (same memory, same stack) │
│ │
└─────────────────────────────────────┘

Examples:

  • calling a function in the same Python module
  • calling a method on a C# class in the same assembly
  • one domain object invoking another object directly

This is the tightest form of coupling. If Module B changes its function signature, Module A may break immediately — sometimes at compile time, sometimes at runtime.

Use this when the collaborating code belongs to the same deployable unit and should evolve together.


[2] Interface / Virtual Dispatch — Nanoseconds

Section titled “[2] Interface / Virtual Dispatch — Nanoseconds”

Same process and same memory, but the caller talks through an abstraction: an interface, abstract class, trait, protocol, function pointer, or callback.

┌──────────────────────┐
Module A ───▶ │ IRepository │
│ (interface/contract) │
└──────────▲───────────┘
│ implements
┌───────────┴───────────┐
│ │
SqlRepo MemoryRepo

This adds one layer of indirection — for example, a vtable lookup — but it enables:

  • dependency injection
  • test doubles and mocking
  • swapping implementations
  • separating policy from detail
  • depending on contracts rather than concrete classes

This is the SOLID / Dependency Inversion Principle sweet spot: nearly the same runtime speed as direct calls, but looser coupling at the source-code level.

Use this when you want modularity inside one process without paying network or serialization costs.


[3] Dynamic Loading / Plugins — Nanoseconds After Load

Section titled “[3] Dynamic Loading / Plugins — Nanoseconds After Load”

Plugins are code modules loaded at runtime: shared libraries, assemblies, extensions, or packages discovered dynamically.

Once loaded, calls can be nearly as fast as static calls. The main complexity is not latency; it is compatibility.

┌──────────────────── Host Application ────────────────────┐
│ │
│ Stable Core ───▶ Plugin Contract ───▶ Loaded Plugin A │
│ └────▶ Loaded Plugin B │
│ │
└──────────────────────────────────────────────────────────┘

Examples:

  • Python C extensions, such as NumPy internals
  • .NET assembly loading
  • VS Code extensions
  • Photoshop plugins
  • OsiriX / Horos DICOM viewer plugins
  • browser extensions

The hard part is keeping the contract stable. If the plugin API or ABI changes, older plugins may break even though they are loaded successfully.

Use this when you need a stable core with open-ended extension points.


[4] Inter-Process Communication on the Same Host — Microseconds

Section titled “[4] Inter-Process Communication on the Same Host — Microseconds”

Here, two processes run on the same machine. They do not share the same stack or heap by default, so the operating system mediates communication.

┌─────────────┐ ┌─────────────┐
│ Process A │ ─────── IPC ───────▶ │ Process B │
└─────────────┘ └─────────────┘
┌──────────────────────────────────────────────────────────┐
│ IPC mechanisms │
│ │
│ • shared memory fastest, hardest │
│ • Unix domain sockets fast, common on Unix-like systems │
│ • named pipes simple, stream-based │
│ • mmap files shared via filesystem │
│ • local TCP convenient, slightly more overhead│
└──────────────────────────────────────────────────────────┘

Examples:

  • PostgreSQL backend processes communicating locally
  • Docker CLI talking to the Docker daemon
  • system services on macOS or Linux
  • a desktop app communicating with a local helper process
  • AI inference workers isolated from a web/API process

PostgreSQL, for example, can use Unix sockets for local connections and TCP for remote connections.

Use same-host IPC when you want process isolation, crash boundaries, privilege separation, or independent runtime environments, but still want lower latency than remote networking.


[5] Synchronous Network RPC — Milliseconds

Section titled “[5] Synchronous Network RPC — Milliseconds”

This is where most service-to-service communication lives. The caller sends a request over the network and waits for a response.

Client ─────────── request ───────────▶ Server
◀────────── response ──────────
caller blocks until reply
ProtocolFormatNotes
gRPCProtobuf over HTTP/2Fast, strongly typed, streaming, polyglot
REST / HTTPJSON over HTTP/1.1 or HTTP/2Ubiquitous, human-readable, weakly typed by default
GraphQLJSON over HTTPClient selects fields, usually one endpoint
WebSocketAny payload over TCPBidirectional, persistent connection
SOAPXML over HTTPLegacy enterprise systems
DICOMweb / DIMSEDICOM over HTTP / TCPPACS, imaging, radiology workflows

Typical latency:

  • same LAN: roughly sub-millisecond to a few milliseconds
  • same cloud region: often a few milliseconds
  • cross-region or public internet: tens to hundreds of milliseconds

Coupling moves from code signatures to API contracts. The caller and server can be written in different languages and deployed separately, but they must still agree on request shape, response shape, errors, versioning, and availability.

Use synchronous RPC when the caller truly needs an answer now: authentication, validation, query results, command acknowledgement, or interactive user-facing workflows.


[6] Asynchronous Messaging — Milliseconds to Seconds

Section titled “[6] Asynchronous Messaging — Milliseconds to Seconds”

The sender publishes a message to a broker. A receiver consumes it later. The sender does not need to wait, and often does not even know which service will consume the message.

┌────────────── Broker ──────────────┐
Producer ─────▶ │ [msg] [msg] [msg] │ ─────▶ Consumer A
│ queue / topic / stream │ ─────▶ Consumer B
└─────────────────────────────────────┘

Two main shapes:

  1. Queue / work distribution — one message is handled by one consumer.
  2. Topic / publish-subscribe — one event can be observed by many consumers.

Common tools:

ToolCommon shapeNotes
RabbitMQQueue / pub-subFlexible routing, classic message broker
AWS SQSQueueManaged work distribution; one message usually goes to one consumer
KafkaLog / stream / pub-subDurable ordered event log, replayable consumers
NATSPub-sub / request-replyLightweight, fast, cloud-native messaging
Redis StreamsStreamUseful when Redis is already present
MQTTPub-subLightweight messaging for IoT/sensor telemetry
AWS SNSPub-subManaged cloud event broadcast
Google Pub/SubPub-subManaged event distribution
Azure Service BusQueue / topicsEnterprise messaging on Azure

In short:

  • Queue / work distribution: RabbitMQ, AWS SQS — one message → one consumer.
  • Pub/sub / event broadcast: Kafka, NATS, Redis Streams, MQTT — one event → many consumers.

Examples:

  • order processing
  • AI inference jobs being queued
  • sensor and telemetry pipelines
  • notifying multiple services that a study was uploaded

Async messaging changes the design vocabulary. Instead of asking, “What function do I call?”, you ask:

  • What event happened?
  • Who owns the event schema?
  • Can the event be replayed?
  • Is message handling idempotent?
  • What happens if the consumer is down?
  • How do we trace a workflow across services?

The benefits are resilience and decoupling. Producers and consumers can scale independently. Temporary failures can be absorbed by queues. New consumers can be added without changing the producer.

The costs are operational complexity: retries, duplicate delivery, ordering, poison messages, schema evolution, observability, and eventual consistency.

Use asynchronous messaging when work can happen later, when multiple consumers may care about the same event, or when you need to buffer spikes between services.


[7] Batch / File Exchange / Eventual — Seconds to Hours

Section titled “[7] Batch / File Exchange / Eventual — Seconds to Hours”

The loosest form of communication is not a call at all. Systems exchange files, rows in a shared database, scheduled dumps, or callbacks that happen later.

System A ───── writes ─────▶ [S3 bucket / FTP / shared DB]
System B ◀──── reads later ───────────┘
on schedule / trigger

Examples:

  • CSV or Excel exports
  • nightly ETL jobs into a data warehouse
  • SFTP drops
  • object-storage handoff through S3, GCS, or Azure Blob
  • dropping a CSV in cloud storage for a partner
  • webhook callbacks: “we’ll POST to your URL when ready”
  • DICOM C-STORE batches to a research archive
  • DICOM studies arriving into a watched folder
  • HL7 batch interfaces
  • data lake ingestion
  • research datasets exported for offline analysis

This pattern is slow, but it is extremely useful. It works across organizations, firewalls, legacy systems, and weak integration environments. Many hospital and enterprise systems still rely on this style because it is inspectable, recoverable, and operationally simple.

The main risks are stale data, unclear ownership, duplicate imports, partial files, naming conventions, schema drift, and weak observability.

Use batch/file exchange when real-time interaction is unnecessary, when systems are organizationally far apart, or when reliability and auditability matter more than immediacy.


The further right you go, the less the systems are coupled by code — but coupling never disappears. It changes form.

LevelCoupled byTypical failure mode
Direct callfunction signaturecompile/runtime break
Interfacecontract in source codeincompatible implementation
PluginABI/API and lifecycleplugin load failure
Same-host IPClocal protocol and OS resourcesdeadlocks, permissions, process crash
Sync RPCnetwork API contracttimeout, 5xx, version mismatch
Async messagingevent schema and broker semanticsduplicate events, ordering, poison messages
Batch/filefile format, location, schedulestale data, partial imports, schema drift

This is the key mental model: decoupling is not free. You are moving coupling from one place to another.


A practical decision guide:

If you need…Prefer…
maximum speed inside one deployable unitdirect calls
testability and replaceable implementationsinterfaces / dependency injection
third-party or optional extension pointsplugins
process isolation on one machinesame-host IPC
immediate answer from another servicesynchronous RPC
resilience, buffering, fan-out, or event workflowsasynchronous messaging
cross-organization exchange or offline workflowsbatch/file exchange

Where should you draw the boundary?

Inside one team's code, hot path → [1] [2]
Plugin / extensibility → [3]
Co-located performance-critical services → [4]
Microservices, request/response → [5]
Decoupled, scale-independent events → [6]
Cross-org, batch, audit-friendly workflows → [7]

Each step looser gives you independence — deployment, scaling, language choice, failure isolation, and organizational separation — at the cost of latency, complexity, and consistency guarantees.

Most real systems mix several tiers. A service might use in-process modules internally [1–2], expose a REST API at the edge [5], publish Kafka events behind the scenes [6], and export nightly files to a partner [7].

A simple heuristic:

Start as tight as your ownership boundary allows.
Loosen communication when deployment, resilience, scaling, ownership, or organizational boundaries demand it.

Do not turn every method call into a network call. Do not turn every event into a batch job. Architecture is mostly about placing the boundary in the right place.


Communication patterns are not just transport choices. They define the shape of the system.

  • Direct calls create a modular codebase.
  • Interfaces create replaceable internals.
  • Plugins create an extension ecosystem.
  • IPC creates local process boundaries.
  • RPC creates distributed services.
  • Messaging creates event-driven systems.
  • Batch exchange creates data pipelines and institutional handoffs.

The more distributed the system becomes, the more the design shifts from function calls to contracts, schemas, retries, observability, and operational discipline.

That is the trade: speed and simplicity on one side, independence and resilience on the other.