What is Message Passing?

Message Passing is the mechanism by which objects communicate in OOP — one object sends a message to another object requesting it to perform an action. In OOP, a “message” is simply a method call: the sender names the method and supplies arguments; the receiver decides how to respond. It is the foundation of all inter-object collaboration.

Explanation

Real-World Analogy

  • Think of restaurant ordering 🍽️:
    • You (client object) don’t cook the food yourself.
    • You send a message to the waiter (receiving object): "I want pasta, medium spicy".
    • The waiter decides how to fulfil it — you don’t know if the chef changes, the kitchen method changes, or there’s a delay.
    • This is encapsulated message passing: the sender knows what to ask, not how it’s done.

Why Message Passing Matters

BenefitExplanation
DecouplingSender only knows the interface, not the implementation
PolymorphismSame message, different handlers (duck typing, method overriding)
EncapsulationReceiver controls its own state; the sender can’t touch internals
ExtensibilityAdd new receivers without changing senders
TestabilityMock receivers easily in unit tests

Anatomy of a Message

  • A message has three parts:
receiver.method_name(arguments)
┌──────────┐ ┌──────────────┐ ┌──────────────┐
│ Receiver │ │ Message Name │ │  Arguments   │
│ (object) │ │ (method)     │ │ (parameters) │
└──────────┘ └──────────────┘ └──────────────┘

Example:
account.deposit(500)
 ^^^^^^^  ^^^^^^^  ^^^
receiver  message  arg

Synchronous vs Asynchronous Message Passing

SynchronousAsynchronous
ExecutionCaller blocks until receiver respondsCaller continues immediately
ExampleRegular method callasync/await, callbacks, actor model
Use caseSimple operations, math, gettersI/O, network, long computations
Error handlingExceptions propagate directlyFutures, error callbacks
Synchronous:                 Asynchronous:

Sender ──call()──► Receiver  Sender ──send()──► Queue ──► Receiver
       ◄──result──           Sender continues...         processes later

How It Works

Method Dispatch

  • When you send a message, the runtime performs method dispatch — finding the right method to call:
animal.speak()

Runtime lookup:
1. What is the actual type of `animal`? → Dog
2. Does Dog have a `speak()` method?    → Yes → call Dog.speak()
3. If not, check parent class            → Animal.speak()
4. If not found anywhere                 → AttributeError / NoSuchMethodError
  • This lookup at runtime is called dynamic dispatch (for virtual methods) or static dispatch (resolved at compile time).

Message Passing vs Direct Field Access

Message PassingDirect Field Access
Approachaccount.get_balance()account.balance
Encapsulation✅ Preserved❌ Breaks encapsulation
Validation✅ Can validate inside method❌ No validation
Flexibility✅ Can change internals freely❌ Callers depend on field name
OOP PrincipleCorrectAnti-pattern

Implementation

# ─── Python ──────────────────────────────────────────────────────────
# Message Passing: BankAccount communicates only via method calls
 
class BankAccount:
    def __init__(self, owner: str, balance: float = 0.0):
        self._owner = owner          # Private state — no direct access
        self._balance = balance
        self._history: list[str] = []
 
    # Message: "deposit"
    def deposit(self, amount: float) -> None:
        if amount <= 0:
            raise ValueError("Deposit must be positive")
        self._balance += amount
        self._history.append(f"+{amount:.2f}")
        print(f"[{self._owner}] Deposited ${amount:.2f}. Balance: ${self._balance:.2f}")
 
    # Message: "withdraw"
    def withdraw(self, amount: float) -> bool:
        if amount > self._balance:
            print(f"[{self._owner}] Insufficient funds.")
            return False
        self._balance -= amount
        self._history.append(f"-{amount:.2f}")
        print(f"[{self._owner}] Withdrew ${amount:.2f}. Balance: ${self._balance:.2f}")
        return True
 
    # Message: "get_balance"
    def get_balance(self) -> float:
        return self._balance
 
    # Message: "transfer" — triggers messages on another object
    def transfer(self, amount: float, target: "BankAccount") -> None:
        if self.withdraw(amount):                   # Message to self
            target.deposit(amount)                  # Message to another object
            print(f"Transfer of ${amount:.2f} complete.")
 
 
class ATM:
    """ATM only communicates via messages — never touches account internals."""
    def process(self, account: BankAccount, action: str, amount: float) -> None:
        if action == "deposit":
            account.deposit(amount)           # Send "deposit" message
        elif action == "withdraw":
            account.withdraw(amount)          # Send "withdraw" message
        else:
            print(f"Unknown action: {action}")
 
 
# ── Client code ─────────────────────────────────────────────────────
alice = BankAccount("Alice", 1000.0)
bob   = BankAccount("Bob",   500.0)
atm   = ATM()
 
atm.process(alice, "deposit", 200)    # ATM → alice.deposit(200)
atm.process(alice, "withdraw", 300)   # ATM → alice.withdraw(300)
alice.transfer(400, bob)              # alice → self.withdraw(400) → bob.deposit(400)
print(f"Alice: ${alice.get_balance():.2f}, Bob: ${bob.get_balance():.2f}")
// ─── C++ ─────────────────────────────────────────────────────────────
#include <iostream>
#include <string>
#include <vector>
#include <stdexcept>
 
class BankAccount {
    std::string owner_;
    double balance_;
    std::vector<std::string> history_;
 
public:
    BankAccount(std::string owner, double balance = 0.0)
        : owner_(std::move(owner)), balance_(balance) {}
 
    // Message: "deposit"
    void deposit(double amount) {
        if (amount <= 0) throw std::invalid_argument("Must be positive");
        balance_ += amount;
        history_.push_back("+" + std::to_string(amount));
        std::cout << "[" << owner_ << "] Deposited $" << amount
                  << ". Balance: $" << balance_ << "\n";
    }
 
    // Message: "withdraw"
    bool withdraw(double amount) {
        if (amount > balance_) {
            std::cout << "[" << owner_ << "] Insufficient funds.\n";
            return false;
        }
        balance_ -= amount;
        history_.push_back("-" + std::to_string(amount));
        std::cout << "[" << owner_ << "] Withdrew $" << amount
                  << ". Balance: $" << balance_ << "\n";
        return true;
    }
 
    double getBalance() const { return balance_; }
 
    // Message: "transfer" — sends messages to self and target
    void transfer(double amount, BankAccount& target) {
        if (withdraw(amount)) {           // Message to self
            target.deposit(amount);       // Message to another object
            std::cout << "Transfer of $" << amount << " complete.\n";
        }
    }
};
 
int main() {
    BankAccount alice("Alice", 1000.0);
    BankAccount bob("Bob", 500.0);
 
    alice.deposit(200);       // Send "deposit" message
    alice.withdraw(300);      // Send "withdraw" message
    alice.transfer(400, bob); // Send "transfer" message → cascades
 
    std::cout << "Alice: $" << alice.getBalance()
              << ", Bob: $" << bob.getBalance() << "\n";
}
// ─── Java ─────────────────────────────────────────────────────────────
import java.util.*;
 
class BankAccount {
    private final String owner;
    private double balance;
    private final List<String> history = new ArrayList<>();
 
    BankAccount(String owner, double balance) {
        this.owner = owner;
        this.balance = balance;
    }
 
    // Message: "deposit"
    public void deposit(double amount) {
        if (amount <= 0) throw new IllegalArgumentException("Must be positive");
        balance += amount;
        history.add("+" + amount);
        System.out.printf("[%s] Deposited $%.2f. Balance: $%.2f%n", owner, amount, balance);
    }
 
    // Message: "withdraw"
    public boolean withdraw(double amount) {
        if (amount > balance) {
            System.out.printf("[%s] Insufficient funds.%n", owner);
            return false;
        }
        balance -= amount;
        history.add("-" + amount);
        System.out.printf("[%s] Withdrew $%.2f. Balance: $%.2f%n", owner, amount, balance);
        return true;
    }
 
    public double getBalance() { return balance; }
 
    // Message: "transfer" — cascading messages
    public void transfer(double amount, BankAccount target) {
        if (withdraw(amount)) {          // Message to self
            target.deposit(amount);      // Message to target object
            System.out.printf("Transfer of $%.2f complete.%n", amount);
        }
    }
}
 
class MessagePassingDemo {
    public static void main(String[] args) {
        BankAccount alice = new BankAccount("Alice", 1000.0);
        BankAccount bob   = new BankAccount("Bob",   500.0);
 
        alice.deposit(200);        // Send "deposit" message
        alice.withdraw(300);       // Send "withdraw" message
        alice.transfer(400, bob);  // Cascading message passing
 
        System.out.printf("Alice: $%.2f, Bob: $%.2f%n",
            alice.getBalance(), bob.getBalance());
    }
}
// ─── JavaScript ───────────────────────────────────────────────────────
class BankAccount {
    #owner;
    #balance;
    #history = [];
 
    constructor(owner, balance = 0) {
        this.#owner = owner;
        this.#balance = balance;
    }
 
    // Message: "deposit"
    deposit(amount) {
        if (amount <= 0) throw new Error("Must be positive");
        this.#balance += amount;
        this.#history.push(`+${amount}`);
        console.log(`[${this.#owner}] Deposited $${amount}. Balance: $${this.#balance}`);
    }
 
    // Message: "withdraw"
    withdraw(amount) {
        if (amount > this.#balance) {
            console.log(`[${this.#owner}] Insufficient funds.`);
            return false;
        }
        this.#balance -= amount;
        this.#history.push(`-${amount}`);
        console.log(`[${this.#owner}] Withdrew $${amount}. Balance: $${this.#balance}`);
        return true;
    }
 
    get balance() { return this.#balance; }
 
    // Message: "transfer" — cascading messages
    transfer(amount, target) {
        if (this.withdraw(amount)) {    // Message to self
            target.deposit(amount);    // Message to another object
            console.log(`Transfer of $${amount} complete.`);
        }
    }
}
 
const alice = new BankAccount("Alice", 1000);
const bob   = new BankAccount("Bob",   500);
 
alice.deposit(200);         // Send "deposit" message
alice.withdraw(300);        // Send "withdraw" message
alice.transfer(400, bob);   // Cascading message passing
 
console.log(`Alice: $${alice.balance}, Bob: $${bob.balance}`);
// ─── C# ──────────────────────────────────────────────────────────────
using System;
using System.Collections.Generic;
 
class BankAccount {
    private string owner;
    private double balance;
    private List<string> history = new();
 
    public BankAccount(string owner, double balance) {
        this.owner = owner;
        this.balance = balance;
    }
 
    // Message: "Deposit"
    public void Deposit(double amount) {
        if (amount <= 0) throw new ArgumentException("Must be positive");
        balance += amount;
        history.Add($"+{amount}");
        Console.WriteLine($"[{owner}] Deposited ${amount:F2}. Balance: ${balance:F2}");
    }
 
    // Message: "Withdraw"
    public bool Withdraw(double amount) {
        if (amount > balance) {
            Console.WriteLine($"[{owner}] Insufficient funds.");
            return false;
        }
        balance -= amount;
        history.Add($"-{amount}");
        Console.WriteLine($"[{owner}] Withdrew ${amount:F2}. Balance: ${balance:F2}");
        return true;
    }
 
    public double Balance => balance;
 
    // Message: "Transfer" — cascading
    public void Transfer(double amount, BankAccount target) {
        if (Withdraw(amount)) {       // Message to self
            target.Deposit(amount);   // Message to target object
            Console.WriteLine($"Transfer of ${amount:F2} complete.");
        }
    }
 
    static void Main() {
        var alice = new BankAccount("Alice", 1000.0);
        var bob   = new BankAccount("Bob",   500.0);
 
        alice.Deposit(200);
        alice.Withdraw(300);
        alice.Transfer(400, bob);
 
        Console.WriteLine($"Alice: ${alice.Balance:F2}, Bob: ${bob.Balance:F2}");
    }
}

Asynchronous Message Passing

  • In modern systems, objects often communicate asynchronously using queues, futures, or the Actor Model (e.g., Erlang, Akka, Python's asyncio).

Python Async Example

import asyncio
 
class NotificationService:
    async def send_email(self, to: str, subject: str) -> str:
        """Async message — simulates I/O delay."""
        await asyncio.sleep(1)  # Simulate network call
        return f"Email sent to {to}: {subject}"
 
class UserRegistration:
    def __init__(self, notifier: NotificationService):
        self.notifier = notifier
 
    async def register(self, email: str) -> None:
        print(f"Registering {email}...")
        # Async message: caller doesn't block waiting for email to send
        result = await self.notifier.send_email(email, "Welcome!")
        print(result)
 
async def main():
    notifier = NotificationService()
    reg = UserRegistration(notifier)
    await reg.register("alice@example.com")
 
asyncio.run(main())

Key Takeaways

  • Every method call is a messageobj.method(args) = send method message to obj with args.
  • Sender knows what to ask, not how it’s done — preserves encapsulation.
  • Dynamic dispatch resolves the correct handler at runtime based on the receiver’s actual type.
  • Synchronous messages block the caller; asynchronous messages (callbacks, async/await, actors) do not.
  • Message passing is the mechanism that makes polymorphism work in practice.
  • Related: Encapsulation, Polymorphism, dynamic-binding, Loose Coupling and High Cohesion

More Learn

GitHub & Webs