CHAPTER 8 · ARCHITECTURE

Software Architectural Patterns

Chapters 3–5 taught you patterns for arranging classes inside one program. This chapter zooms out: how do you arrange whole components — servers, services, databases, message brokers — into a system that is fast, available, and changeable? You'll meet nine classic architectural patterns, trace requests through each one, and learn how to pick an architecture from a list of requirements.

From classes to systems: zooming out

A design pattern organises a handful of classes inside one process; an architectural pattern organises the whole system — often across many processes and machines. When you applied Strategy or Decorator, everything lived in a single running program: one address space, method calls that cost nanoseconds, no network in sight. Architecture is what you see when you zoom out far enough that individual classes blur into boxes labelled "web server", "business logic", "database" — and the lines between the boxes become network calls that can be slow, can fail, and can be intercepted.

Design pattern (GoF)Architectural pattern
ScaleA few classes / objectsWhole subsystems, processes, machines
UnitsClasses, interfaces, methodsComponents: services, tiers, brokers, databases
ConnectionsMethod calls, object referencesConnectors: HTTP calls, message queues, sockets, shared DBs
Typical question"How do I add a new payment strategy without editing existing code?""How do I survive a server crash during Black Friday?"
Cost of changing your mindA refactoring sessionOften a rewrite — architecture is the hardest thing to change later
ExamplesStrategy, Observer, Facade3-tier, MVC, SOA, Pub/Sub, Microservices

What "architecture" actually means

A software architecture is usually described with three ingredients — worth memorising because every pattern in this chapter is just a different recipe using them:

Components
The units of computation or storage: a web front-end, a business-logic service, a database, a message broker, a cache. In the diagrams on this page, components are the boxes.
Connectors
How components communicate: HTTP request/response, remote procedure calls, publish/subscribe messages, shared files or databases. The arrows between the boxes — and each kind of arrow has very different failure and performance behaviour.
Constraints
The rules the pattern imposes: "the presentation tier may never talk to the database directly", "publishers must not know who subscribes". Constraints are the whole point — they are what buys you the pattern's benefits.

Quality attributes: the forces that pick the architecture

Functional requirements ("the user can transfer money") tell you what code to write. On their own they often don't discriminate between candidate architectures — you could implement a funds transfer as a desktop app, a 3-tier web app, or fifty microservices. That's why, between otherwise-viable options, the force that usually picks the architecture is the quality attributes (also called non-functional requirements, or "the -ilities"):

Quality attributePlain-language meaningPattern it pushes you towards
ScalabilityCan it handle 10× the users by adding hardware, not rewriting code?Tiers, clusters, pub/sub, microservices
AvailabilityDoes it stay up when a machine dies? ("five nines" = 99.999% uptime ≈ 5 min downtime/year)Clusters, peer-to-peer, redundancy everywhere
PerformanceLow latency (fast responses) and high throughput (many requests/second)Fewer network hops, caching, load balancing
MaintainabilityCan a new developer change the discount rule without touching UI or SQL?Layers/tiers, MVC — separation of concerns
InteroperabilityCan it talk to the 30-year-old mainframe and the new mobile app?SOA, brokers, ESBs
SecurityCan we keep attackers away from the crown jewels (the data)?Tiers with firewalls between them, proxies
🎓 Lecture view vs reality

For the exam: quality attributes (the "-ilities") are the forces that pick between architectures. In practice: functional requirements shape architecture too — real-time deadlines, offline operation, geographic distribution, regulatory/privacy rules, and long-running transactional workflows are all functional needs that can force a particular structure. Think of it this way: functional requirements decide which architectures are even viable, and quality attributes discriminate between the survivors.

🧠 Why this matters

The slides say architectural patterns exist "for different problems: performance problems, high-availability problems". That's the key mental model: each pattern is a trade — it buys some quality attributes by paying with others (usually simplicity, latency, or operational effort). There is no best architecture, only a best fit for your forces. The summary table at the end of this page makes every trade explicit.

⚠️ Watch out

Architecture decisions are the most expensive decisions to reverse in a project. Renaming a class takes seconds; converting a 2-tier desktop app into a 3-tier web system is months of work. That's why we study these patterns before writing code — the same reason GoF patterns exist, but with much higher stakes.

Tiered architectures: 3-tier & MVC

3-Tier architecture architectural

Intent: split the system into three physically separate tiers — presentation, business logic, and data — where each tier talks only to its immediate neighbour.

🌍 Real-world analogy

A restaurant. The dining room (presentation) is where customers interact; the kitchen (business logic) applies the recipes and rules; the storeroom (data) holds the ingredients. Customers never walk into the storeroom and grab flour — every request goes through the kitchen, which enforces the recipes. You can renovate the dining room without changing a single recipe, and reorganise the storeroom without the customers noticing.

The problem: the "fat client"

Before tiers, the common shape was 2-tier client–server: a desktop application (the "fat client") installed on every user's PC, talking straight to a shared database server. All the business rules live inside the desktop app:

❌ 2-tier fat client: rules + SQL inside the UI
// Runs on EVERY customer-service PC in the company
public void OnCheckoutClicked()
{
    // The database password ships to 500 desktops...
    using var conn = new SqlConnection(
        "Server=HQ-DB01;Database=Shop;User=app;Password=s3cret!");
    conn.Open();

    decimal total = LoadCartTotal(conn, _cartId);

    // Business rule buried in a button handler:
    if (total > 100m) total *= 0.95m;   // 5% discount

    var cmd = new SqlCommand(
        $"INSERT INTO Orders (CartId, Total) VALUES ({_cartId}, {total})",
        conn);
    cmd.ExecuteNonQuery();
    MessageBox.Show($"Order placed: {total:C}");
}
Business logic, SQL, and UI in one blob, replicated onto every desktop.
✅ 3-tier: each concern in its own tier
// TIER 1 — Presentation (browser / thin UI)
//   renders HTML, sends "POST /checkout" — contains NO rules.

// TIER 2 — Business logic (application server)
public class OrderService
{
    private readonly IOrderRepository _repo;
    public OrderService(IOrderRepository repo) => _repo = repo;

    public Receipt Checkout(int cartId)
    {
        var cart = _repo.LoadCart(cartId);
        decimal total = cart.Items.Sum(i => i.Price * i.Qty);
        if (total > 100m) total *= 0.95m;  // the rule lives HERE, once
        int orderId = _repo.SaveOrder(cartId, total);
        return new Receipt(orderId, total);
    }
}

// TIER 3 boundary — Data access
public interface IOrderRepository
{
    Cart LoadCart(int cartId);
    int SaveOrder(int cartId, decimal total);
}
Change the discount rule → redeploy one server. Clients and DB untouched.

The change request that kills the fat client: "the discount rule changes to 7% next Monday." Now you must rebuild and reinstall the app on 500 desktops — and until every machine is updated, different users apply different rules. Worse: every desktop holds database credentials (a security nightmare), and the database is doing all the shared work, so it becomes the bottleneck. The fix is to pull the rules out of the client into a middle tier that all clients share.

The structure

TIER 1 · Presentation TIER 2 · Business logic TIER 3 · Data Client / Browser renders UI, captures input Application server all business rules Database server storage, transactions HTTP(S) SQL Constraint: tier 1 never talks to tier 3 directly. Dashed = response.
Figure: 3-tier architecture — linear, each tier only knows its neighbour.

How a request flows

  1. User acts — clicks "Checkout" in the browser. The presentation tier's only job is to turn that into a request: POST /checkout.
  2. Business tier validates and decides — the application server authenticates the user, loads the cart, applies pricing and discount rules.
  3. Business tier asks the data tier — it sends SQL (or an ORM call) to the database server; only the app server's credentials can reach the DB, which sits behind a second firewall.
  4. Data tier answers — the database executes the transaction and returns rows.
  5. Business tier shapes the result — computes the receipt, maybe caches, logs, audits.
  6. Presentation renders — the browser receives the response and displays it. No rule was evaluated on the client.

You can watch this animated in the Request Tracer demo below.

🔀 Easily confused: tier vs layer (the classic exam trap)

A layer is a logical separation; a tier is a physical one. Layers are how you organise code (presentation layer, business layer, data-access layer — possibly all in one .NET solution, running in one process). Tiers are how you organise deployment (browser machine, app-server machine, DB machine — separate processes, usually separate hardware, connected by a network). Every 3-tier system is also 3-layered, but a 3-layered system can run in a single tier — a desktop app with neat UI/Services/Data folders is layered, not tiered. Tiers buy you independent scaling and security boundaries; layers only buy you maintainability. People (and job ads) use the words interchangeably — you now know better.

Generalising: n-tier. Nothing stops you at three. Real systems often insert more physical tiers: a web tier (serves static content, terminates HTTPS) in front of an application tier, a caching tier (Redis) beside it, an integration tier talking to partner systems. Hence the generic name n-tier architecture. The constraint stays the same: requests flow through adjacent tiers, never skipping.

🎓 Lecture view vs reality

For the exam: the classic 3-tier model has physically separate tiers with strictly closed layering — each tier calls only its immediate neighbour, never skipping. In practice: "tier" (a deployment/physical boundary) and "layer" (a logical grouping of code) are separate ideas, and layering comes in two flavours. A closed layer may be called only by the layer directly above it (the slide's version); an open layer may be bypassed — for example a shared caching or utility layer that any layer above is allowed to call directly. Real systems routinely relax strict closure for pragmatic reasons; the strictly-closed physical form is the textbook baseline, not a hard rule.

Benefits & drawbacks

✅ Benefits❌ Drawbacks
Change business rules in one place — redeploy one tierEvery request pays 2+ network hops → higher latency than a monolithic desktop app
Each tier scales independently (add app servers without touching the DB)More machines, more configuration, more things to monitor
Security in depth: DB behind a firewall, clients never hold DB credentialsStrictly linear — everything funnels through the middle tier, which can become a bottleneck
Teams can specialise per tier (front-end / back-end / DBA)The middle tier can degenerate into a "big ball of mud" if not layered internally

Real-world examples: almost every web application you use — online banking, e-commerce, university portals. In the .NET world the canonical stack is: Blazor/React front-end (tier 1) → ASP.NET Core app on IIS/Kestrel (tier 2) → SQL Server (tier 3), and a typical solution mirrors it with MyApp.Web, MyApp.Services, MyApp.Data projects — layers that can be deployed as tiers.

Model–View–Controller (MVC) architectural

Intent: isolate business logic from the user interface by splitting into three roles — the Model (data + business logic), the View (what the user sees), and the Controller (interprets user input and coordinates the other two) — so views and logic can change with minimal impact on each other.

🌍 Real-world analogy

A cricket match. The match itself — runs, wickets, overs — is the Model: the single source of truth. The scoreboards around the ground are Views: each displays the same match state in its own style, and you can add a TV graphics overlay (a new View) without changing how cricket is scored. The scorer is the Controller: they watch events (input), decide what they mean ("that's four runs"), and update the match record — they never paint the scoreboard themselves.

The problem: UI and logic welded together

The naive UI app puts everything in the event handler — the same disease as the fat client, but at class level:

❌ Naive: logic inside the view
public void OnFourButtonClicked()
{
    _runs += 4;                      // business logic...
    if (_runs >= _target)
        _resultLabel.Text = "WON!";  // ...fused to widgets
    _scoreLabel.Text = $"Score: {_runs}";
}
The rule "reaching the target wins" only exists inside a button handler of one specific screen.
✅ MVC: three separable roles
// MODEL — knows the rules, knows nothing about screens
public class Match
{
    public int Runs { get; private set; }
    public bool Won => Runs >= Target;
    public int Target { get; init; } = 120;
    public event Action? Changed;              // Observer!
    public void AddRuns(int r) { Runs += r; Changed?.Invoke(); }
}
Any number of views can render this model; the win rule exists exactly once and is unit-testable with no UI.

The change request that kills the naive version: "we also need a JSON API for the mobile app, and a big-screen view for the stadium." With logic welded into one screen's handlers, you either duplicate the rules per view (and they will drift apart) or start passing widgets around. MVC's answer: put the rules in a Model that doesn't know screens exist, and let each View be a cheap, dumb renderer of it.

The triangle

Controller interprets input, coordinates View renders the model Model data + business logic ① user input ② updates ③ notifies / view reads state
Figure: MVC communication is triangular — compare with 3-tier's straight line.

How a request flows (ASP.NET Core MVC flavour)

The lecture slides show the Java EE version of web MVC — JSP pages as Views, a Servlet as Controller, JavaBeans/EJBs as the Model (Java EE, now Jakarta EE, is Java's enterprise platform: application servers that host these components and handle transactions, security and scaling for them). The identical idea in the C# world is ASP.NET Core MVC:

  1. Request arrivesGET /products/42. The framework's router maps the URL to a controller action: ProductsController.Details(42).
  2. Controller coordinates — it calls into the Model (domain services/entities): var p = _catalog.Find(42);. No HTML here, no SQL here.
  3. Model does the real work — business rules run, the repository fetches data.
  4. Controller picks a Viewreturn View(p); chooses Details.cshtml and hands it the model data.
  5. View renders — the Razor template turns the model into HTML. Swap it for a JSON serialiser and the same controller + model now serve a mobile API.
MiniMvc.cs — the triangle in 30 lines (console, no framework)
// VIEW — dumb renderer; subscribes to the model (Observer pattern)
public class ScoreboardView
{
    private readonly Match _match;
    public ScoreboardView(Match match)
    {
        _match = match;
        _match.Changed += Render;          // ③ model notifies view
    }
    public void Render() =>
        Console.WriteLine($"[view] SCORE {_match.Runs}/{_match.Target}" +
                          (_match.Won ? "  — WON!" : ""));
}

// CONTROLLER — translates raw input into model operations
public class MatchController
{
    private readonly Match _match;
    public MatchController(Match match) => _match = match;
    public void HandleInput(string key)    // ① input arrives here
    {
        if (key == "4") _match.AddRuns(4); // ② controller updates model
        if (key == "6") _match.AddRuns(6);
    }
}

// Wiring: var m = new Match(); new ScoreboardView(m);
// new MatchController(m).HandleInput("6");
// → [view] SCORE 6/120
ℹ️ Note — MVC is built on Observer

Step ③ (model notifies views) is literally the Observer pattern: the Model is the subject, Views are observers. That's why the Model can stay ignorant of its Views — it just raises Changed. This is your first taste of a theme: architectural patterns are often design patterns scaled up (Pub/Sub, later on this page, is Observer scaled across a network).

🔀 Easily confused: classic MVC vs ASP.NET Core "MVC"

Classic (Smalltalk-80) MVC is what the triangle above shows: the View observes the Model directly and re-reads it whenever it changes (edge ③ is literally Observer), and the Controller handles raw user input. ASP.NET Core MVC — and Rails, Spring MVC, Django — is a front-controller web variant: a single front controller/router dispatches each HTTP request to a controller action; the controller builds a model and hands it to a view, which renders once and returns HTML. There is no live Model→View observation, because HTTP is request/response and the view is regenerated per request. Same three role names, meaningfully different mechanics — the MiniMvc console demo above is the classic form, while the "How a request flows" steps describe the web form.

How MVC relates to 3-tier (slide: "3-Tier and MVC")

Students constantly conflate these two, and the slides call the difference out explicitly:

3-TierMVC
ShapeLinear — all communication passes through the middle tierTriangular — View → Controller → Model, and View reads/gets notified by Model directly
Kind of splitPhysical deployment (where code runs)Logical responsibility (what code does)
Question it answers"How do we deploy, scale and secure?""How do we keep UI and logic independently changeable?"

They compose rather than compete. In a modern web app, MVC typically lives inside the tiers: the View renders in the browser (tier 1), the Controller and Model run on the app server (tier 2), and the Model persists itself via the data tier (tier 3) — that's the slides' "MVC across two tiers". In a rich single-page app, an entire MVC triangle can live in the client tier alone ("MVC — client tier" in the slides), with the server reduced to an API. So the honest answer to "is my app 3-tier or MVC?" is usually: both.

Benefits & drawbacks

✅ Benefits❌ Drawbacks
Multiple views over one model (HTML + JSON + big screen) with zero duplicated logicFor tiny UIs, three roles is ceremony — a to-do list doesn't need it
Model is unit-testable without any UI runningThe triangle is easy to get wrong: logic leaks into controllers ("fat controller" anti-pattern)
UI specialists and domain developers work in parallelDeciding what belongs in Controller vs Model takes judgement and team discipline

Real-world examples: ASP.NET Core MVC and Razor Pages, Ruby on Rails, Django (calls it MTV, same triangle), Spring MVC, Laravel; on the desktop, cousins like MVVM (WPF's INotifyPropertyChanged binding is the "model notifies view" edge made framework-level) and MVP. Chapter 11 puts MVC, MVP and MVVM side by side.

Decentralised systems: Peer-to-Peer

Peer-to-Peer (P2P) architectural

Intent: connect nodes in an ad-hoc manner without central control — the concepts of "client" and "server" disappear because every node plays both roles interchangeably.

🌍 Real-world analogy

A restaurant vs a potluck dinner. Client–server is the restaurant: one kitchen cooks for everyone — great control, but if the kitchen burns down nobody eats, and a big crowd overwhelms it. P2P is the potluck: every guest brings a dish and eats. More guests means more food, not less; no single failure ruins dinner; but nobody can guarantee what's on the menu.

The problem: the central server is a bottleneck, a bill, and a target

In client–server, all load and all trust concentrate in one place. Three ways that hurts:

The structure

Client–Server (star) Server client client client client Peer-to-Peer (mesh) peer peer peer peer peer
Figure: the star concentrates load and failure in one node; the mesh spreads both.

How a request flows (BitTorrent-style file sharing)

  1. Join — your node starts up and discovers a few other peers (from a lightweight tracker or a distributed hash table — see the deep dive below).
  2. Query — "who has file X?" The question propagates peer-to-peer; several peers answer "I have pieces 1–40", "I have 30–80".
  3. Download in parallel — you fetch different pieces from different peers simultaneously. Here you are acting as a client.
  4. Upload while downloading — other peers fetch from you the pieces you already have. Now you are the server. Both roles at once — this is the defining property.
  5. Seed — after completing, you keep serving. The swarm's total capacity grew because you joined: popularity makes P2P faster, the exact opposite of client–server.
Deep dive: how do peers find things without a server? (unstructured vs structured P2P)

Unstructured (Gnutella, KaZaA): peers connect randomly and searches flood the network — ask your neighbours, they ask theirs, up to a hop limit. Simple and robust, but a search may miss rare files and floods waste bandwidth. KaZaA reduced this with super-peers: beefy nodes that index their neighbourhood (a partial re-centralisation).

Structured (BitTorrent's DHT, Chord, Kademlia): peers form a distributed hash table — each key (e.g. a file's hash) is deterministically assigned to a node, and any node can route a lookup to it in about log(N) hops. Guarantees you find what exists, at the cost of maintenance traffic as peers churn (join/leave constantly).

Benefits & drawbacks

✅ Benefits❌ Drawbacks
No single point of failure — nodes die, the network shrugsNo single point of control either: hard to update, moderate, or secure
Scales with demand: every consumer is also a supplierPeers are unreliable — they churn, lie, and freeload; protocols need incentives (BitTorrent's tit-for-tat) and verification (hashes)
Cheap: no data-centre bill for the operatorDiscovery/search is much harder than querying one server
Resistant to censorship and DDoSInconsistent performance; NAT/firewall traversal headaches

Real-world examples: BitTorrent (file distribution — even Windows Update and game launchers use P2P delivery between machines on a LAN); blockchain networks like Bitcoin and Ethereum (every full node stores, validates and relays the ledger — consensus without a central bank is P2P's boldest application); early Skype (calls routed through super-peers); the slides' historical examples Napster and KaZaA. Note that many "P2P" systems are hybrids: a tiny central piece for bootstrapping, a decentralised core for the heavy lifting.

🔀 Easily confused

P2P vs client–server is a spectrum, not a switch. Napster = central index + P2P transfer. Modern multiplayer games = central matchmaking + sometimes P2P game traffic. Ask "which functions are centralised?" rather than "is it P2P?".

Service-based architectures: SOA, Proxy/Broker, Service Locator

Service-Oriented Architecture (SOA) architectural

Intent: segment a large system's functionality into distributed services — network-accessible modules with published interfaces — register them in a registry, and compose them into business processes with an orchestrator. SOA is the direct predecessor of microservices (chapter 9).

🌍 Real-world analogy

A large company. Each department (payroll, legal, IT) offers well-defined services with official request forms — you don't walk into payroll and edit the spreadsheet yourself. A company directory (the registry) tells you which department does what, and a project manager (the orchestrator) chains departments together to deliver something no single department could: "legal approves → finance pays → IT provisions."

The problem: integration spaghetti

Enterprises accumulate systems for decades: a COBOL mainframe for accounts, a 1990s CRM, a Java billing system, a new mobile backend. Each new business process ("let customers transfer funds from the app") needs several of them. The naive approach is a point-to-point integration for every pair that must talk — custom file drops, direct DB reads, bespoke sockets. With N systems you head towards N×(N−1)/2 fragile, undocumented links; nobody dares touch the mainframe because seventeen unknown things read its tables directly. The change request that kills it: "replace the CRM" — and suddenly you must find and rewrite every hidden link into it.

SOA's fix: every capability is wrapped as a service with an explicit, published contract, and consumers depend only on contracts — never on tables, files, or internals. Composition happens in one visible place instead of everywhere.

The components (per the slides)

🎓 Lecture view vs reality

For the exam: SOA means distributed services described by WSDL, discovered through a UDDI registry, talking SOAP over an ESB. In practice: that stack is the 2000s enterprise incarnation of SOA, not its definition. SOA is a broader architectural style — "build the system from network-accessible services behind published contracts" — and the SOAP/WSDL/UDDI/ESB combination is just one (now dated) way to realise it. By that broader definition, microservices are arguably a lightweight, decentralised SOA: same core idea, dumb pipes instead of a smart ESB.

Mobile app Web portal Partner system Service orchestration (ESB) routing · transformation · process flow [SOAP/REST/POX] Registry (UDDI / name service) ① Registration service ② Authentication service ③ Balance Inquiry service ④ Fund Transfer service look up register
Figure: the slides' SOA landscape — consumers → orchestration/ESB → distributed services, with a registry on the side.

How a request flows ("transfer LKR 5,000 to Mum")

  1. Consumer calls the process, not the pieces — the mobile app sends one request, TransferFunds, to the orchestrator. It has no idea four services exist.
  2. Orchestrator resolves services — it consults the registry (or its cached bindings) for the current endpoints of Authentication, Balance Inquiry and Fund Transfer.
  3. Authentication service — validates the user's session/token.
  4. Balance Inquiry service — the ESB may here transform the request into the fixed-width record format the mainframe expects. Sufficient funds? Continue.
  5. Fund Transfer service — performs the debit/credit; the orchestrator handles a failure by compensating (e.g. reversing the debit) and logging.
  6. One response returns — the orchestrator aggregates results and answers the app.
IFundTransferService.cs — a contract-first service (WCF-era C#)
// In .NET's SOA era (WCF), attributes like these generated the WSDL
// contract automatically — clients in Java or COBOL middleware could
// consume it without ever seeing your C#.
[ServiceContract]
public interface IFundTransferService
{
    [OperationContract]
    TransferResult Transfer(string fromAccount, string toAccount, decimal amount);
}

public record TransferResult(bool Success, string Reference, string? Error);

SOA vs microservices (the teaser for chapter 9)

SOA (2000s)Microservices (2010s→)
Integration brainCentralised: smart ESB, dumb services' edges ("smart pipes")Decentralised: dumb pipes (plain HTTP/queues), smart endpoints
Service sizeCoarse — often a whole subsystem per serviceFine — one business capability per service
DataServices often share enterprise databasesEach service owns its database
ProtocolsSOAP/WSDL/UDDI, XML everywhereREST/gRPC/JSON, lightweight discovery
GoalIntegrate heterogeneous enterprise systemsIndependent deployment and scaling per team

Microservices kept SOA's core insight (business capabilities as independently deployable services behind contracts) and dropped the heavyweight middleware — the ESB had itself become a bottleneck and a single point of change. Full story in chapter 9.

Benefits & drawbacks

✅ Benefits❌ Drawbacks
Reuse: one Authentication service serves every channel (web, mobile, branch)Heavy middleware: the ESB needs its own team, and becomes a change bottleneck
Legacy systems join modern processes via adapters — no rewriteXML/SOAP overhead; every hop through the bus adds latency
Replace a system behind its contract without touching consumersContracts + registry + governance = real bureaucracy for small systems
Business processes are explicit, auditable, in one placeOrchestrator can accumulate business logic that belongs in services

Real-world examples: core banking (exactly the slides' example), airline reservation ecosystems, government one-stop portals stitching decades-old departmental systems, telecom BSS/OSS stacks. In .NET, WCF + BizTalk Server was Microsoft's classic SOA toolkit.

Knowledge & event-driven patterns: Blackboard, Pub/Sub

Blackboard architectural

Intent: for problems with no deterministic solution strategy, let independent specialist knowledge sources cooperatively build a solution on a shared data structure (the blackboard), coordinated by a control component — the solution emerges incrementally rather than following a fixed pipeline.

🌍 Real-world analogy

Detectives around a case board. Nobody knows the murderer up front and there is no recipe to find one. The forensics expert pins fingerprint results to the board; seeing them, the records clerk pins a matching criminal file; seeing that, the alibi checker crosses a name off. Each specialist watches the board, contributes when their expertise applies, and reads others' contributions. The lead detective (control) decides who investigates next and declares when the case is solved. No specialist ever talks to another directly — only through the board.

The problem: no algorithm gets you from input to answer

Take speech recognition (the pattern's original home, the 1970s Hearsay-II system). Raw audio must become text, but every stage is uncertain: a phoneme detector produces guesses ("that sound is 70% 'b', 30% 'p'"), word hypotheses depend on phonemes and on grammar, grammar depends on words… A naive fixed pipeline — audio → phonemes → words → sentence, each stage feeding the next — breaks because information must flow both ways: recognising the word "architecture" upstream can retroactively fix a misheard phoneme downstream. The change request that kills the pipeline: "use sentence-level context to correct phoneme guesses" — a fixed one-way pipeline simply has no channel for that.

The structure (three parts, straight from the slides)

Blackboard
The shared, evolving knowledge base: hypotheses at every level (signals, phonemes, words, phrases), each usually tagged with a confidence score.
Knowledge sources
Independent specialists — humans, agents, or modules — each watching for the kind of data it can act on (by pattern-matching the board) and writing new or improved hypotheses back.
Control / moderator
Decides which eligible knowledge source runs next (priorities, confidence, cost), and when to stop — solution good enough, or dead end.
Blackboard signal → phoneme → word hypotheses + confidence KS: Phoneme detector KS: Word matcher KS: Grammar analyser Control picks next KS, stops monitors
Figure: knowledge sources read/write the blackboard; control moderates. Specialists never call each other.

How a "request" flows (speech recognition, one cycle at a time)

  1. Seed — the raw audio signal is written onto the blackboard.
  2. Control scans — which knowledge sources can contribute right now? The phoneme detector can (there's a fresh signal); the grammar analyser can't yet.
  3. KS activates — the phoneme detector writes hypotheses: "b-a-k…? p-a-k…?" with confidences.
  4. New data enables new specialists — the word matcher now sees phonemes and writes "back (0.6), pack (0.4)".
  5. Higher knowledge corrects lower — the grammar analyser sees "…sent the ___ up" and boosts "pack"; it may even revise the phoneme hypothesis. This backward correction is what pipelines can't do.
  6. Control stops — when a sentence hypothesis crosses the confidence threshold (or no KS can improve anything), control declares the answer.
Blackboard.cs — the skeleton in C#
public class Blackboard
{
    // hypothesis name → (value, confidence)
    public Dictionary<string, (object Value, double Confidence)> Data { get; } = new();
    public bool Has(string key) => Data.ContainsKey(key);
}

public interface IKnowledgeSource
{
    string Name { get; }
    bool CanContribute(Blackboard bb);   // pattern-match the board
    void Contribute(Blackboard bb);      // write new/better hypotheses
}

public class Control
{
    private readonly List<IKnowledgeSource> _sources;
    public Control(List<IKnowledgeSource> sources) => _sources = sources;

    public void Run(Blackboard bb)
    {
        bool progress = true;
        while (progress && !Solved(bb))
        {
            // control policy: here, first eligible KS wins; real systems
            // rank candidates by expected value of their contribution
            var ks = _sources.FirstOrDefault(s => s.CanContribute(bb));
            progress = ks != null;
            if (ks != null)
            {
                Console.WriteLine($"[control] activating {ks.Name}");
                ks.Contribute(bb);
            }
        }
    }
    private bool Solved(Blackboard bb) =>
        bb.Has("sentence") && bb.Data["sentence"].Confidence > 0.9;
}

Benefits & drawbacks

✅ Benefits❌ Drawbacks
Handles problems with no known deterministic algorithm — partial, uncertain, multi-level knowledgeHard to test and to reason about: the execution order is decided at runtime
Specialists are fully decoupled — add a new KS without touching the othersNo guarantee of a solution, or of how long it will take
Supports experimentation: swap control strategies, tune confidencesThe blackboard is a shared hotspot — concurrency control gets tricky

Real-world examples (from the slides): speech recognition, vehicle identification and tracking, protein structure identification, sonar signal interpretation — all "AI problems" where evidence accumulates from noisy sources. Modern echoes: sensor-fusion stacks in self-driving cars and multi-agent AI systems that share a common working memory.

🔀 Easily confused: Blackboard vs Pub/Sub vs Mediator

All three decouple communicating parties, but: Blackboard is data-centric — parties share evolving state and a controller decides who acts (pull). Pub/Sub (next) is event-centric — fire-and-forget messages pushed to whoever subscribed; no shared state, no controller choosing turns. Mediator is the in-process cousin where a hub routes interactions between known colleagues. Full side-by-sides in chapter 12.

Publish/Subscribe (Pub/Sub) architectural

Intent: publishers broadcast messages on named topics (subjects) to a broker (messaging server / middleware); subscribers tell the broker which topics interest them. Publishers and subscribers never know each other — they are decoupled in space, time, and synchronisation.

🌍 Real-world analogy

A magazine. The publisher prints an issue about photography and hands it to the postal service — it has no list of readers in its head. You subscribed to the "photography" topic at the post office, so a copy lands in your mailbox; your neighbour subscribed to "cooking" and gets nothing. The publisher doesn't know you exist (space decoupling); the issue waits in your mailbox while you're on holiday (time decoupling); and the printing press never stops to wait for you to finish reading (synchronisation decoupling).

The problem: the publisher that knows everybody

❌ Naive: direct calls to every interested party
public class OrderService
{
    private readonly InventoryService _inventory;
    private readonly BillingService _billing;
    private readonly EmailService _email;
    // next sprint: + AnalyticsService, + FraudCheck, ...

    public void PlaceOrder(Order order)
    {
        Save(order);
        _inventory.Reserve(order);   // blocks until done
        _billing.Charge(order);      // blocks until done
        _email.SendReceipt(order);   // email server down? order fails!
    }
}
OrderService must know, reference, and wait for every consumer — and be edited for each new one.
✅ Pub/Sub: announce and move on
public class OrderService
{
    private readonly IMessageBroker _broker;
    public OrderService(IMessageBroker broker) => _broker = broker;

    public void PlaceOrder(Order order)
    {
        Save(order);
        _broker.Publish("orders.placed", order.ToJson());
        // done. Zero knowledge of who listens. Adding an
        // analytics consumer = a new subscription, not an edit here.
    }
}
One outgoing edge, to the broker. Consumers subscribe to orders.placed independently.

The change request that kills the naive version: "add fraud detection, a loyalty points service, and a warehouse dashboard — and by the way, the email service being down must not stop order placement." Direct calls mean editing OrderService for every new consumer (an OCP violation at system scale) and coupling its uptime to all of theirs. Pub/Sub inverts the dependency: consumers opt in, the publisher just announces facts.

The structure

Publisher 1 (OrderService) Publisher 2 (SensorGateway) Messaging server (broker / middleware) topic: orders topic: sensors Subscriber: Billing Subscriber: Email Subscriber: Dashboard publish deliver
Figure: the slides' picture — publishers → messaging middleware (topics) → subscribers. No arrow ever goes publisher → subscriber.

How a message flows

  1. Subscribe — Billing and Email each tell the broker "I want orders". The broker records the subscriptions; publishers are not involved and may not even exist yet (time decoupling).
  2. Publish — OrderService sends {orderId: 42, ...} to topic orders and immediately continues. It doesn't block, doesn't retry per-consumer, doesn't know the count of listeners (space + synchronisation decoupling).
  3. Broker fans out — it delivers a copy to every current subscriber of orders. A durable subscription is even held for subscribers that are temporarily offline — they catch up on reconnect.
  4. Subscribers react independently — Billing charges the card, Email sends the receipt. One of them crashing affects nobody else.
  5. New consumer? No code changes upstream — the Dashboard team subscribes to orders next month; OrderService is never edited or redeployed.
MessageBroker.cs — a toy in-process broker
public interface IMessageBroker
{
    void Subscribe(string topic, string name, Action<string> handler);
    void Publish(string topic, string message);
}

public class MessageBroker : IMessageBroker
{
    private readonly Dictionary<string, List<(string Name, Action<string> Handler)>>
        _topics = new();

    public void Subscribe(string topic, string name, Action<string> handler)
    {
        if (!_topics.TryGetValue(topic, out var subs))
            _topics[topic] = subs = new();
        subs.Add((name, handler));
    }

    public void Publish(string topic, string message)
    {
        if (!_topics.TryGetValue(topic, out var subs)) return; // nobody cares — fine!
        foreach (var (name, handler) in subs)
        {
            Console.WriteLine($"[broker] {topic} → {name}");
            handler(message);   // a real broker queues this asynchronously
        }
    }
}
ℹ️ Pub/Sub is Observer, scaled out

Squint at the broker code: Subscribe/Publish is Observer's attach/notify with a string topic in the middle. The differences that make it architectural: an intermediary process (the broker) so publisher and subscribers live in different programs on different machines; delivery is asynchronous (Observer's Notify() is a synchronous loop in one thread); and subscribers can be offline at publish time yet still receive the message (durability). Observer couples subject→observer objects; Pub/Sub couples both sides only to a topic name. See chapter 12 for the full comparison.

Try it: Pub/Sub playground — topics, subscriptions, and the coupling count
What to notice: publish to a topic with several subscribers — one send, many deliveries, and the publisher's code never named a receiver. Publish to an empty topic — nothing breaks; the message just has no audience. Then watch the coupling counter: with direct point-to-point calls, every publisher would need a reference to every interested consumer (P × S edges that all break when something changes); via the broker each party has exactly one edge (P + S). That difference is the whole argument for Pub/Sub.

Benefits & drawbacks

✅ Benefits (straight from the slides)❌ Drawbacks
Loose coupling — parties know only topic namesThe broker is critical infrastructure: it must itself be clustered or it's your new single point of failure
Asynchronous — publishers never wait; spikes are absorbed by queuesDebugging is harder: "who consumed this message, and when?" needs tracing tools
Inherent scalability — ideal for "a high and fluctuating number of publishers and subscribers"Delivery semantics are subtle: at-least-once means duplicates; ordering across topics isn't guaranteed
Add/remove consumers with zero publisher changesNo reply by default — request/response needs extra machinery (correlation IDs, reply topics)

Real-world examples: the slides list the classic broker products — JMS (Java Messaging Service — Java EE's standard pub/sub API), RabbitMQ, TIBCO, IBM MQ Series, BizTalk — and the classic scenarios: a stock exchange feed (one price tick published, thousands of trading systems each subscribed to the symbols they care about) and IoT (thousands of sensors publish readings over MQTT, a lightweight pub/sub protocol; dashboards, alarms and loggers subscribe). Also: event notifications, distributed caching & logging, live chat and multiplayer collaboration. Modern .NET equivalents: Azure Service Bus topics, Kafka with confluent-dotnet, Redis pub/sub, and in-process, MediatR notifications or C# events.

Proxy / Broker architectural

Intent: let a client call a remote service as if it were a local object. The service publishes a proxy (a local stand-in implementing the same interface) to a look-up service; the client downloads the proxy and calls methods on it; the proxy handles all the networking — marshalling arguments, sending them to the real implementation, returning results. The broker is the middleman infrastructure making this work.

🌍 Real-world analogy

An embassy. You need something from a foreign government (the remote server), but you don't fly there — you visit its local embassy (the proxy). The embassy speaks your language, accepts your forms, and handles the international communication with the home country on your behalf. To you it feels like dealing with the country directly, from your own city. And you found the embassy's address in a directory (the look-up service).

The problem: remote calls are nothing like local calls

Suppose your client needs GetBalance("ACC-42") from a service on another machine. Naively you'd hand-write socket code in the client: open a TCP connection, invent a wire format ("GETBAL|ACC-42\n"), serialise arguments, parse responses, handle timeouts — for every method of every service, duplicated in every client. The change request that kills it: "add a parameter to GetBalance" — now every hand-rolled client is silently broken, and the compiler can't warn anyone because the contract only exists as string conventions. The fix: define the contract once as an interface, and let generated proxies do the plumbing, checked by the type system.

The mechanism (the slides' five steps)

  1. Define the service with an interface — e.g. IAccountService. The interface is the contract.
  2. Create a proxy of that interface for the given implementation — typically generated, not hand-written.
  3. Upload the proxy to a look-up service — the server registers "I implement IAccountService, here's the proxy/stub for reaching me".
  4. Client downloads the proxy from the look-up service (this is where Service Locator comes in — the slides note the two patterns complement each other).
  5. Client calls the proxy like a local object — the proxy marshals the call, ships it across the network to the real implementation, and unmarshals the result.
Client calls IAccountService Proxy (local) marshals · sends · waits Look-up service registry of proxies Remote server real implementation ① register + upload proxy ② download ③ local call ④ network call (RMI / CORBA / gRPC) ⑤ result travels back through the proxy
Figure: the client only ever touches the interface; the proxy + broker hide the network.
AccountServiceProxy.cs — what a generated proxy amounts to
// The contract — shared by client and server
public interface IAccountService
{
    decimal GetBalance(string accountNo);
}

// Client side: same interface, but the body is plumbing.
// (Tools generate this: RMI stubs, CORBA, WCF svcutil, gRPC codegen.)
public class AccountServiceProxy : IAccountService
{
    private readonly string _endpoint;
    public AccountServiceProxy(string endpoint) => _endpoint = endpoint;

    public decimal GetBalance(string accountNo)
    {
        Console.WriteLine($"[proxy] marshal GetBalance({accountNo})");
        Console.WriteLine($"[proxy] → send to {_endpoint}, wait for reply");
        // real proxy: serialize → TCP/HTTP → server skeleton invokes
        // the actual AccountService → result serialized back
        return 1250.75m; // demo stand-in for the unmarshalled reply
    }
}

// Client code cannot tell it's remote:
// IAccountService svc = registry.Lookup<IAccountService>();
// Console.WriteLine(svc.GetBalance("ACC-42"));

Session Facade (the slides' J2EE special case)

The slides add a related idea from the Java EE world: a session facade — a stateless session bean that sits in front of a cluster of fine-grained entity beans (persistent objects). Remote clients call one coarse method on the facade (TransferFunds(from, to, amount)) instead of making many fine-grained remote calls (getAccount, getBalance, setBalance…). Two wins: the client sees a simpler interface with the implementation complexity hidden — that's the Facade pattern — and you replace N slow network round-trips with one. The modern .NET equivalent is an API controller or application-service class exposing one endpoint per use case, not per entity property.

Benefits & drawbacks

✅ Benefits❌ Drawbacks
Location transparency: clients code against interfaces, not addressesThe leaky abstraction trap: a call that looks local can be 10,000× slower and can fail in ways local calls can't (timeouts, partial failure)
Contract checked by the compiler; plumbing generated, not hand-rolledVersioning: proxy and server must agree on the contract version
Broker can add security, logging, retries invisiblyBroker/look-up service is extra infrastructure to run and secure

Real-world examples: the slides' Java RMI and CORBA (the 1990s cross-language broker standard); .NET Remoting and WCF client proxies; today's gRPC generated clients — same pattern, protobuf-flavoured.

🔀 Easily confused: Broker vs the GoF Proxy pattern

The design-pattern Proxy is one class standing in for another in the same process (lazy loading, access control). The architectural Proxy/Broker is the same "stand-in with the same interface" idea stretched across a network, plus the infrastructure (look-up service, marshalling) that stretching requires. Remote proxy is literally listed as a variant of GoF Proxy — this is the cleanest example of a design pattern scaling up into an architectural one. See chapter 12.

Service Locator architectural

Intent: distributed services register themselves in a central registry; clients look up the service they need by name or type instead of hard-coding where (or what) it is. This gives location transparency — the client doesn't know or care which machine serves it.

🌍 Real-world analogy

Directory enquiries / the phone book. You don't memorise the plumber's number (hard-coded address); you look up "plumber" and call whoever is listed today. If the plumbing company moves offices or a new company takes over the listing, your lookup routine doesn't change — only the registry entry does.

The problem: hard-wired locations and constructions

In a distributed system, writing new TcpClient("192.168.1.40", 9000) in the client welds it to one machine. The change request that kills it: "we moved the payments service to new hardware" (or "we now run three instances") — every client needs a code change and redeployment. The slides' cure: services register themselves in a registry — JNDI (Java's naming directory), the RMI Registry, UDDI, the CORBA naming service — and clients resolve at runtime:

  1. Service starts → registers — "I am IPaymentService, reach me at host X, port Y."
  2. Client asks the locator — "give me an IPaymentService." No address in client code.
  3. Locator consults the registry (and typically caches the answer) and hands back a reference — often a proxy; the two patterns complement each other.
  4. Client calls the service through the returned reference, oblivious to its location. Tomorrow the registry can point somewhere else; the client never changes.
Client no addresses inside Service Locator lookup + cache Registry JNDI · UDDI · RMI reg. Service registers itself on start ① lookup("payments") ② resolve register ③ call the service via the returned reference/proxy
Figure: Service Locator — clients depend on names, the registry maps names to locations.
ServiceLocator.cs — the in-process version
public static class ServiceLocator
{
    private static readonly Dictionary<Type, object> _services = new();

    public static void Register<T>(T implementation) where T : class
        => _services[typeof(T)] = implementation;

    public static T Resolve<T>() where T : class
        => _services.TryGetValue(typeof(T), out var s)
             ? (T)s
             : throw new InvalidOperationException(
                   $"No service registered for {typeof(T).Name}");
}

// Startup:  ServiceLocator.Register<IPaymentService>(new StripePayments());
// Anywhere: var pay = ServiceLocator.Resolve<IPaymentService>();

The "Service Locator is an anti-pattern" debate — both sides, fairly

Modern .NET advice says: don't use the in-process locator above; use dependency injection (DI) instead — the technique where a container passes dependencies into constructors, which you met with DIP. Why did DI win?

❌ Locator: dependencies are invisible
public class CheckoutService
{
    public Receipt Checkout(Cart cart)
    {
        // Nothing in this class's signature reveals
        // these needs — you find out by reading bodies:
        var pay = ServiceLocator.Resolve<IPaymentService>();
        var tax = ServiceLocator.Resolve<ITaxCalculator>();
        // forgot to Register ITaxCalculator in a test?
        // → runtime explosion, not compile error
        ...
    }
}
Hidden dependencies, runtime failures, tests must set up a global static — and everything depends on the locator itself.
✅ DI: dependencies are the signature
public class CheckoutService
{
    private readonly IPaymentService _pay;
    private readonly ITaxCalculator _tax;

    // The constructor IS the honest list of needs.
    // Forget one? The compiler / container tells you
    // at startup, not mid-request.
    public CheckoutService(IPaymentService pay,
                           ITaxCalculator tax)
        => (_pay, _tax) = (pay, tax);
}
Explicit, compile-checked, trivially testable: new CheckoutService(fakePay, fakeTax).

The fair verdict: the anti-pattern criticism (popularised by Mark Seemann) targets using a locator inside application classes as a lazy substitute for DI — there it genuinely hides dependencies and hurts testability. But the pattern is not universally bad: (1) in its original, distributed sense — finding remote services via a registry — it's alive and essential: microservice service discovery (Consul, Eureka, Kubernetes DNS, chapter 9) is exactly Service Locator at network scale; (2) DI containers themselves use locator-style resolution internally (IServiceProvider.GetService() in ASP.NET Core is a service locator — the guidance is that the framework may call it, your business classes shouldn't); (3) at genuine framework boundaries (plugin hosts, middleware activation) a lookup is sometimes the only option. Rule of thumb: resolve at the composition root, inject everywhere else.

Benefits & drawbacks

✅ Benefits❌ Drawbacks
Location transparency; services can move/scale without client changesIn-process use hides dependencies → the DI critique above
Central place to swap implementations (test doubles, regions, versions)The registry is critical shared infrastructure — cache or cluster it
Foundation for runtime discovery in elastic systemsLookup failures happen at runtime, not compile time

Real-world examples: JNDI and the RMI registry (slides), UDDI in SOA, DNS itself (name → address is the ur-locator), Kubernetes service DNS, Consul/Eureka in microservices, and IServiceProvider in ASP.NET Core.

Cluster architectures

Clustering, load balancing & failover architectural

Intent: run multiple identical copies of a tier on separate machines behind a load balancer, so the system survives machine failures (failover) and scales by adding boxes (horizontal scaling).

🌍 Real-world analogy

Supermarket checkouts. One giant checkout (a bigger server — vertical scaling) has a hard limit and a catastrophic failure mode. Instead: many identical tills, plus the queue marshal (load balancer) directing each customer to a free till. A till breaks? The marshal stops sending people there — shoppers barely notice. Christmas rush? Open more tills — no rebuild of the store.

The problem: one machine = one failure away from an outage

Your 3-tier app runs on one app server. Two guaranteed disasters: (1) the machine dies — or just needs an OS patch — and the whole business is down; (2) traffic exceeds what one machine can do, and the only lever is buying an ever-bigger machine, which gets exponentially expensive and still has a ceiling. The change request that kills it: "we need 99.9% uptime and zero-downtime deployments." A single node can't do either, however big. The fix is redundancy: clones + a traffic director.

The moving parts

Single-tier vs multi-tier clustering (the slides' two diagrams)

Single-tier cluster Clients Load balancer Server A (web+app) Server B (web+app) Server C (web+app) Shared DB Multi-tier cluster LB 1 (web) Web 1 Web 2 LB 2 (app) App 1 App 2 App 3 DB cluster (primary + replicas)
Figure: single-tier clustering clones one combined server; multi-tier clustering clusters each tier independently, with a balancer per tier.

Single-tier clustering clones the whole (web+app) server — simple, few machines, fine for moderate scale, but you must scale everything together even if only one layer is busy. Multi-tier clustering gives each tier its own cluster and balancer — the web tier might need 8 nodes while the app tier needs 3; each scales and fails over independently, firewalls can sit between tiers (security), but you now run a fleet. This is n-tier + clustering composed — and one step from here is chapter 9's microservices, where each service is its own little cluster.

How a request flows (including a failure!)

  1. Client → load balancer — clients only ever know the balancer's address (one DNS name).
  2. Health check — the balancer's heartbeat noticed 10 seconds ago that Node B stopped answering; B is out of the rotation.
  3. Route — round-robin picks the next healthy node, say A. If sessions are sticky, your cookie pins you to A instead.
  4. Node A serves the request — reading any session state from the shared store, so it doesn't matter that your last request went to C.
  5. Response returns via the balancer — the user never learns that B died. That's failover: failure without downtime.

Watch exactly this scenario in the Request Tracer — pick "Cluster (failover)".

Benefits & drawbacks (slides list the benefits)

✅ Benefits❌ Drawbacks
High availability — node failure ≠ outage; also enables rolling, zero-downtime deploymentsState is the hard part: sessions, caches and file uploads must move off the nodes
Optimal performance / flexible load balancing — spread load, add nodes on demandThe load balancer itself must be redundant, or it's the new single point of failure
Improved security — balancers hide topology, terminate TLS, absorb some attacks; firewalls between clustered tiersMore machines + orchestration = operational complexity and cost
Ease of administration — patch one node at a time while others serveDatabases cluster far less easily than stateless tiers (replication, consistency)

Real-world examples: every serious website is a web farm behind NGINX/HAProxy or a cloud balancer (AWS ELB, Azure Load Balancer); SQL Server Always On availability groups and Redis Sentinel for the data tier; Kubernetes Deployment replicas + Service are exactly "cluster + load balancer + health checks" turned into declarative configuration.

Interactive: the Request Tracer

Reading about request flows is one thing — watching them is better. Pick an architecture, press Send request, and follow one request hop-by-hop through the boxes. The same user click takes a very different journey depending on the architecture you chose.

Try it: trace a request through each architecture
What to notice: 3-tier is a strict there-and-back line; MVC is a triangle; Pub/Sub fans one publish out to many subscribers after the publisher has already moved on; Proxy/Broker spends most of its steps on finding and reaching the server; the cluster reroutes around a dead node mid-flow. Count the network hops in each — that's latency you pay for the pattern's benefits.

Choosing an architecture

Every pattern buys some quality attributes and pays with others. This table is the whole chapter in one look — read each row as "if these are my dominant forces, start here":

PatternOptimises forSacrificesTypical use
3-tier / n-tierMaintainability, security layering, independent tier scalingLatency (hops), everything funnels through the middleBusiness web apps, e-commerce, banking portals
MVCUI/logic independence; many views over one model; testable modelsCeremony for tiny UIs; discipline needed to keep controllers thinWeb frameworks (ASP.NET Core MVC, Rails), GUI apps
Peer-to-peerNo single point of failure/control; scales with demand; cheap to operateControl, consistency, discovery simplicity, predictable performanceFile sharing (BitTorrent), blockchain, comms
SOAReuse across the enterprise; integrating heterogeneous/legacy systemsSimplicity; latency through the ESB; heavy governanceEnterprise integration: banks, airlines, government
BlackboardProblems with no deterministic algorithm; pluggable specialistsPredictability, testability, guaranteed terminationSpeech recognition, sensor fusion, AI hypothesis systems
Publish/SubscribeLoose coupling, async scalability, fluctuating consumer countsDebuggability, delivery-semantics simplicity, request/response easeStock tickers, IoT telemetry, event notifications, chat
Proxy/BrokerLocation transparency; typed contracts for remote callsHidden network costs; contract versioning disciplineRMI/CORBA/gRPC-style distributed object systems
Service LocatorRuntime discovery; services move without client changesIn-process: hidden dependencies (prefer DI); registry is critical infraDistributed registries (DNS, Consul, k8s); framework internals
ClusterAvailability, horizontal scale, zero-downtime operationsStatefulness (must externalise sessions), ops complexity, costAny production web tier; database HA setups
Microservices (ch. 9)Independent deployment & scaling per team/capabilityDistributed-system complexity everywhere, all at onceLarge products with many teams (Netflix, Amazon)
💡 Tip — how to answer "which architecture?" questions

Hunt the quality attributes in the scenario, not the domain. "Survive server failure" → cluster. "Consumers appear and disappear, sender mustn't care" → pub/sub. "No central server may exist" → P2P. "Compose a 30-year-old mainframe with new apps" → SOA/ESB. "No deterministic algorithm, cooperating specialists" → blackboard. "Change UI without touching rules" → MVC. Most real systems combine patterns: a clustered 3-tier app whose middle tier uses MVC and publishes events to a broker is completely normal.

Check yourself — which architecture fits?

Next up: SOA's successor gets its own two chapters — how microservices decompose a monolith, and what it costs. And when patterns on this page start blurring together (Observer vs Pub/Sub? Broker vs Proxy vs Facade?), chapter 12 untangles them side by side.