Technical Articles & Tutorials

The Missing Architectural Layer in Web Development

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:

Windows GUI Message Processing

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:

Traditional Web Request Handling

# 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/<int:product_id>') 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:

Django Service Layer Pattern

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

// 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 ( <div> <h1>{product.name}</h1> <button onClick={handleAddToCart}>Add to Cart</button> </div> ); }

// 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:

Java EE Message-Driven Bean

@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:

Elixir GenServer (Actor) Implementation

# 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:

CQRS Command Handler

// 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

// 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.

Simple > Complex (16) Evolution Series (12) Contrarian Takes (9) Architecture (9) Information Mgmt (9) Accounting (5) Unix Philosophy (4) Self-Hosting (4) 90% Cheaper (3)