A critical but rarely discussed gap exists in how web architecture evolved compared to desktop application architecture. This gap helps explain many of the challenges and "bolt-on" solutions we see in modern web development.
The Message-Passing Foundation
Desktop GUI applications, particularly on Windows, were designed from the beginning with an event-driven, message-passing architecture:
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_CLOSE:
DestroyWindow(hwnd);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
case WM_PAINT:
// Handle paint message
return 0;
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
This architecture established several fundamental patterns:
- Event-Driven Design: Components respond to messages rather than being directly called
- Asynchronous Communication: Messages are queued and processed in event loops
- Loose Coupling: Components don't need to know about each other, only about message types
- State Management: State changes are triggered by and communicated through messages
The Web's Request-Response Origins
By contrast, early web frameworks originated from a fundamentally different model:
# Django View
def product_page(request, product_id):
product = get_object_or_404(Product, id=product_id)
return render(request, 'products/detail.html', {'product': product})
# Flask Route
@app.route('/products/')
def product_page(product_id):
product = Product.query.get_or_404(product_id)
return render_template('products/detail.html', product=product)
This request-response paradigm:
- Is Synchronous: Each request is handled directly, synchronously returning a response
- Lacks Event Propagation: No built-in way for one component to broadcast events to others
- Is Stateless by Default: HTTP's statelessness required workarounds like sessions
- Has No Message Queue: No built-in concept of pending messages or events
Consequences of the Missing Layer
This architectural gap has led to numerous challenges in web development:
The "Bolted-On" Ecosystem
- Background Job Systems: Sidekiq, Celery, Bull - retrofitting asynchronous processing
- Message Brokers: RabbitMQ, Kafka, Redis Pub/Sub added as separate systems
- WebSockets Libraries: Socket.io, ActionCable, Channels added for real-time features
- State Management Libraries: Redux, MobX, Vuex created to manage client-side state
These additions are often treated as specialized tools rather than fundamental architectural components, leading to increased complexity and integration challenges.
Framework Attempts to Fill the Gap
Django's "Service Layer"
Django's community has attempted to introduce service layer patterns, but these are primarily code organization strategies rather than true message architectures:
class OrderService:
@staticmethod
def create_order(user, cart_items):
# Business logic here
order = Order.objects.create(user=user)
for item in cart_items:
OrderItem.objects.create(order=order, product=item.product, quantity=item.quantity)
# Direct function call, not message passing
PaymentService.process_payment(order)
EmailService.send_order_confirmation(order)
return order
This is still fundamentally procedural and synchronous, not event-driven.
JavaScript Frameworks and Events
Modern JavaScript frameworks have moved closer to event-driven models:
// React Component with Event Emitter
import EventEmitter from './eventEmitter';
function ProductPage({ product }) {
const handleAddToCart = () => {
// Emit an event instead of direct method call
EventEmitter.emit('cart:add', {
productId: product.id,
quantity: 1
});
};
return (
{product.name}
);
}
// Elsewhere in the application
EventEmitter.on('cart:add', (item) => {
// Handle the event
cartStore.addItem(item);
// Maybe trigger other events
EventEmitter.emit('notification:show', {
message: `Added ${item.quantity} item(s) to cart`
});
});
While this improves decoupling, it's still typically implemented as an addon rather than as a core architectural element.
More Holistic Approaches
Java's Enterprise Messaging
Java EE incorporated messaging as a core architectural component:
@MessageDriven(activationConfig = {
@ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Queue"),
@ActivationConfigProperty(propertyName = "destination", propertyValue = "OrderQueue")
})
public class OrderProcessor implements MessageListener {
@Inject
private OrderService orderService;
@Override
public void onMessage(Message message) {
try {
TextMessage textMessage = (TextMessage) message;
String json = textMessage.getText();
Order order = new Gson().fromJson(json, Order.class);
orderService.processOrder(order);
} catch (JMSException e) {
// Handle exception
}
}
}
Actor Model in Elixir/Phoenix
Elixir's Phoenix framework, built on the Erlang VM, embraces a comprehensive message-passing architecture:
# GenServer (actor) definition
defmodule OrderProcessor do
use GenServer
# API
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
def process_order(order) do
GenServer.cast(__MODULE__, {:process, order})
end
# Server callbacks
def init(state) do
{:ok, state}
end
def handle_cast({:process, order}, state) do
# Process the order
# Send messages to other processes
PaymentProcessor.charge(order.payment_details)
EmailSender.send_confirmation(order.customer_email, order)
{:noreply, state}
end
end
This approach makes message-passing fundamental to the architecture, not an afterthought.
Modern Architectural Patterns Addressing the Gap
CQRS (Command Query Responsibility Segregation)
CQRS formalizes the separation of commands (which change state) from queries (which read state), often using a message-based approach:
// Command handler
class CreateOrderCommandHandler {
constructor(orderRepository, eventBus) {
this.orderRepository = orderRepository;
this.eventBus = eventBus;
}
async handle(command) {
const { userId, items } = command;
// Create and save order
const order = new Order(userId, items);
await this.orderRepository.save(order);
// Publish event
this.eventBus.publish(new OrderCreatedEvent(order.id, userId, items));
return order.id;
}
}
Event Sourcing
Event Sourcing takes this further by modeling the entire system around events:
// Event-sourced aggregate
class Order {
constructor(id) {
this.id = id;
this.items = [];
this.status = 'draft';
this.events = [];
}
addItem(productId, quantity, price) {
if (this.status !== 'draft') {
throw new Error('Cannot add items to non-draft orders');
}
const event = new ItemAddedEvent(this.id, productId, quantity, price);
this.applyEvent(event);
return event;
}
applyEvent(event) {
if (event instanceof ItemAddedEvent) {
this.items.push({
productId: event.productId,
quantity: event.quantity,
price: event.price
});
}
this.events.push(event);
}
}
Why This Matters
The architectural gap in web frameworks has significant implications:
- Increased Complexity: Developers must integrate multiple systems to achieve what could be a unified architecture
- Distributed System Challenges: Microservices require robust message-passing that isn't built into most frameworks
- Real-time Expectations: Users now expect real-time updates and interactions
- Reactive Requirements: Modern UIs need to react to state changes from multiple sources
The Future: Unified Architectures
We're seeing movement toward more unified architectures that incorporate message-passing as a fundamental concept:
- Reactive Frameworks: RxJS, Akka, and similar libraries building reactive foundations
- WebAssembly: Enabling non-web architectures to run in browsers
- GraphQL Subscriptions: Adding real-time capabilities to data fetching
- Service Mesh: Infrastructure-level solutions for service-to-service communication
Conclusion
This architectural gap explains why modern web applications often feel like collections of loosely integrated components rather than cohesive systems. The industry continues to evolve toward more event-driven, message-passing architectures, but still largely treats these as add-ons rather than core architectural elements.
As web applications become more complex and interactive, embracing message-passing as a fundamental architectural concept—as desktop applications did decades ago—seems increasingly necessary.
What's Your Architecture?
How have you addressed the messaging gap in your web applications? Have you built systems with events and messages at their core, or do you integrate separate solutions? Let me know in the comments or contact me directly.