Module 3: Communicating Architecture

8. Project: communicate Mercado's architecture for two audiences

Overview

In this project you'll act as Mercado's architect and produce, from beginning to end, the communication package of a real change —the diagrams and the record that make two very different audiences understand the same architecture—. It's not a new lesson: it's where you assemble everything from the module. The scenario: Mercado is going to open its platform to external vendors by API. It's a big decision —it touches the catalog, orders, security, load— and it has to be communicated to two people who couldn't be more different: the VP of product, who approves the project and wants to understand where it fits in the business, and a new developer, who next week starts building the API and needs to know what pieces Mercado is made of and where they'll touch. A single map doesn't work for both; giving them the same diagram would fail at least one. Your job is to produce the complete package and validate that each piece communicates.

You deliver four things: (1) Mercado's Context for the VP —the world map, five boxes, zero jargon—; (2) Mercado's Container for the new dev —the city map, the deployable pieces with their technologies, including the new external-vendors path—; (3) an ADR that communicates the why of the decision, so whoever arrives later understands why Mercado opened by API; and (4) the executed validation that each map aims at the right level for its audience and passes the hygiene rules —doesn't mix levels, isn't spaghetti—. When you finish, you'll have in your hands exactly what an architect delivers when they really communicate an architecture change.

Connection with the module: this project closes the seven topics. It uses C4 (lesson 2) to choose the levels; it draws the Context and the Container (lesson 3) and decides not to go down to Component or Code because it's not needed for these audiences (lesson 4); it practices the heart of the module, choosing the level for each audience (lesson 5); it adds the ADR as communication of the why (lesson 6); and it validates against the rules that avoid the giant doc and the spaghetti (lesson 7). At the end, it points to module 4: communicating well is necessary but not sufficient for the decision to be executed —you have to lead without authority—, which is what follows in the guide.

The file you hand to two different readers

Think of a building architect —a real one, of brick— who finishes the design of a house and has to "communicate" it. They don't hand over one document: they hand over a file with different pieces for different readers. To the house's owner they show the render: how it will look, how many rooms, where the sun hits —what the owner can evaluate and approve—. To the site foreman they hand the technical plans: measurements, materials, where the beams go —what the builder needs to work—. And in a separate folder they keep the decision notes: why the kitchen goes to the north (for the light), why the load-bearing walls are where they are (for the ground) —the why, for whoever has to modify the house in the future—. No one hands the render to the foreman nor the plans to the owner: each reader receives the piece they can use, and the why is kept for the future.

Your Mercado communication package is that file. The Context is the render (for the VP-owner), the Container are the plans (for the dev-builder), and the ADR are the decision notes (for whoever inherits the system). A single artifact doesn't fulfill the three roles, just as a render is no use to build nor a plan to approve the design. Communicating the architecture is assembling the complete file and knowing which piece goes to each reader.

The assignment: Mercado opens its platform to external vendors

The business context, so you have it clear before drawing. Today Mercado's vendors publish products through the web app, by hand. Leadership wants to allow external vendors —stores that already have their own system— to integrate through a public API: to upload their catalog, sync inventory, and receive orders automatically, without entering the web. This opens Mercado to many more vendors (growth) but adds a new surface: a public API that has to be exposed, authenticated, and protected. The architect has already decided how to do it in broad strokes: a new piece, the Seller API, separate from the internal API, with its own token authentication, that validates and queues what comes from external vendors before touching the real catalog. Your job isn't to re-decide that: it's to communicate it to the two audiences and leave the why recorded.

Deliverable 1: the Context for the VP (the world map)

The VP asks: what does Mercado do, who uses it, and where does opening it to external vendors fit? You give them the Context —five/six boxes, zero technology— with the novelty marked: now there are two classes of vendor, the one who uses the web and the external one who integrates by API.

C4Context
    title Mercado - System Context for the VP (with external vendors)
    Person(customer, "Customer", "Buys products")
    Person(seller, "Seller (web)", "Publishes from Mercado's web")
    Person_Ext(ext_seller, "External Seller", "Store with its own system, integrates by API")

    System(mercado, "Mercado", "Marketplace: connects buyers and sellers")

    System_Ext(payments, "Payment Gateway", "Charges payments")
    System_Ext(carrier, "Carrier API", "Ships orders")

    Rel(customer, mercado, "Searches and buys")
    Rel(seller, mercado, "Publishes through the web")
    Rel(ext_seller, mercado, "Syncs catalog by API")
    Rel(mercado, payments, "Charges")
    Rel(mercado, carrier, "Ships")

How you tell it to the VP: "Here's Mercado and its world. Customers buy; sellers publish. Today they only publish through the web (Seller-web); what we approved is letting external vendors —stores with their own system— connect through an API and sync their catalog automatically. That opens us to many more vendors. The usual dependencies —payments and shipping— don't change." Six boxes, one idea, a decision the VP can evaluate: they see the benefit (more vendors) and can ask about the risk (what does exposing a public API imply?) without drowning in a single technology.

Deliverable 2: the Container for the new dev (the city map)

The dev asks: what pieces is Mercado made of and where do I build the new API? You give them the Container —the deployable pieces with their technologies, including the new Seller API—.

C4Container
    title Mercado - Containers for the dev (with the new Seller API)
    Person(customer, "Customer", "Buys")
    Person(seller, "Seller (web)", "Publishes")
    Person_Ext(ext_seller, "External Seller", "Integrates by API")

    System_Boundary(mercado, "Mercado") {
        Container(web, "Web App", "React", "Storefront and seller panel")
        Container(api, "Internal API", "FastAPI", "Catalog, orders, checkout")
        Container(seller_api, "Seller API", "FastAPI", "Public API: validates and queues external-vendor data")
        ContainerDb(db, "Database", "PostgreSQL", "Products, orders, users")
        Container(search, "Search Index", "Elasticsearch", "Catalog search")
        Container(queue, "Ingest Queue", "Redis", "Queue of catalog changes to validate")
    }

    System_Ext(payments, "Payment Gateway", "Stripe")
    System_Ext(carrier, "Carrier API", "Ships")

    Rel(customer, web, "Uses", "HTTPS")
    Rel(seller, web, "Publishes", "HTTPS")
    Rel(ext_seller, seller_api, "Syncs catalog", "HTTPS/API + token")
    Rel(web, api, "Calls", "JSON/HTTPS")
    Rel(seller_api, queue, "Queues changes", "Redis")
    Rel(api, queue, "Consumes and validates changes", "Redis")
    Rel(api, db, "Reads and writes", "SQL")
    Rel(api, search, "Indexes and queries", "HTTPS")
    Rel(api, payments, "Charges", "HTTPS/API")
    Rel(api, carrier, "Ships", "HTTPS/API")

How you tell it to the dev: "These are the pieces. Customers and web-sellers use the Web App, which calls the Internal API, which is the heart —catalog, orders, checkout— on PostgreSQL and Elasticsearch. The new thing you're going to build is the Seller API: a separate piece, public, with token authentication, that receives what external vendors send, queues it in Redis, and lets the Internal API validate it before touching the real catalog. You separate it from the Internal API on purpose, so the external traffic doesn't touch the heart of the system directly." Eight boxes: the dev knows what to build (Seller API + queue), where it fits, and why it's separated. With this they start coding on Monday with a map, not blind.

Notice the level decision: we do not go down to Component or Code. The dev, to get going and orient themselves, needs the Container; when they get into building the interior of the Seller API, they may want a Component, but that's later and only of that piece. For the communication package of this decision, Context + Container is exactly the right set —neither too much (the VP doesn't need the Container, the dev doesn't need the Code now) nor too little—.

Deliverable 3: the ADR that communicates the why

The diagram shows that the Seller API is separated and queues by Redis. It doesn't say why. That why is what the ADR keeps for the dev who in two years wonders "why doesn't the Seller API write directly to the catalog, as simple as that would be?". The ADR:

# ADR-021: Public Seller API separated, with queue-based ingestion

Status: Accepted | Date: 2026-07-30 | Deciders: architect, catalog lead, platform lead

## Context We're opening Mercado to external vendors that integrate by API. That exposes a new public surface: traffic we don't control, of unpredictable volume, that could contain invalid or malicious data. If that API wrote directly to the catalog (the Internal API and its database), a spike of external traffic or a massive load of bad data would hit the heart of the system —the same one that serves buyers—.

## Decision Expose a Seller API separate from the Internal API, with its own token authentication. What comes from external vendors doesn't touch the catalog directly: it's queued (Redis) and the Internal API consumes, validates, and applies it at its own pace. That way the external traffic is isolated from the core.

## Consequences For:

  • External traffic can't take down or corrupt the catalog: the queue absorbs spikes and the validation filters out the bad.
  • The Seller API can be scaled and protected separately, without touching the Internal API.

Against (the price we accept):

  • Catalog changes from external vendors are not instantaneous: they go through the queue, so there's a delay until they're reflected. We accept it because the core's security is worth more than immediacy.
  • One more piece and one more queue to operate and monitor.

This ADR is the note stuck on the fuse box. When the future dev sees that the ingestion has a delay and thinks "this would be instantaneous if the Seller API wrote directly", the ADR will tell them: yes, we knew; the delay is the conscious price of not letting external traffic touch the core. They won't undo the separation out of ignorance. The why traveled through time.

Worked example: validate the package before delivering it

Before deeming the package good, you run it through the module's rules: does each map aim at the right level for its audience? does each one not mix levels and not be spaghetti? The following code validates the two diagram deliverables —the Context for the VP and the Container for the dev— against the three rules at once, and gives the verdict.

# Project: communicate Mercado's architecture to TWO audiences.
# Deliverable 1: a Context for the VP.  Deliverable 2: a Container for the new dev.
# Here we validate that each map (a) aims at the right level for its audience and
# (b) passes the hygiene rules (doesn't mix levels, isn't spaghetti).

LEGIBLE_LIMIT = 20
level_rank = {"Context": 1, "Container": 2, "Component": 3, "Code": 4}
recommended = {"VP of product": "Context", "new dev on the team": "Container"}

CORE_RANK = {"container": 2, "component": 3, "class": 4, "method": 4}
RANK_NAME = {2: "Container", 3: "Component", 4: "Code"}


def mixes_levels(elements):
    ranks = {CORE_RANK[e["type"]] for e in elements if e["type"] in CORE_RANK}
    return len(ranks) > 1


def is_spaghetti(elements):
    return len(elements) > LEGIBLE_LIMIT


# Deliverable 1: Context for the VP.
context_for_vp = {
    "level": "Context",
    "audience": "VP of product",
    "elements": [
        {"type": "person", "name": "Customer"},
        {"type": "person", "name": "Seller"},
        {"type": "software_system", "name": "Mercado"},
        {"type": "external_system", "name": "Payment Gateway"},
        {"type": "external_system", "name": "Carrier API"},
    ],
}

# Deliverable 2: Container for the new dev.
container_for_dev = {
    "level": "Container",
    "audience": "new dev on the team",
    "elements": [
        {"type": "person", "name": "Customer"},
        {"type": "container", "name": "Web App"},
        {"type": "container", "name": "API"},
        {"type": "container", "name": "Checkout Service"},
        {"type": "container", "name": "Database"},
        {"type": "container", "name": "Search Index"},
        {"type": "external_system", "name": "Payment Gateway"},
        {"type": "external_system", "name": "Carrier API"},
    ],
}


def validate(deliverable):
    name = deliverable["audience"]
    level = deliverable["level"]
    want = recommended[name]
    elems = deliverable["elements"]

    fit = "OK" if level == want else "BAD"
    mix = mixes_levels(elems)
    spa = is_spaghetti(elems)

    print(f"Deliverable for: {name}")
    print(f"  Level: {level}  | audience expects: {want}  -> fit: {fit}")
    print(f"  Elements: {len(elems)}  | mixes levels: {'yes' if mix else 'no'}"
          f"  | spaghetti: {'yes' if spa else 'no'}")
    ok = (fit == "OK") and (not mix) and (not spa)
    print(f"  VERDICT: {'COMMUNICATES' if ok else 'REVIEW'}")
    print()
    return ok


print("== Validation of Mercado's two maps ==")
print()
r1 = validate(context_for_vp)
r2 = validate(container_for_dev)
print("Summary: the VP gets 5 boxes with no jargon; the dev gets the city with its pieces.")
print(f"Both deliverables communicate: {r1 and r2}")

What to expect. Running it:

== Validation of Mercado's two maps ==

Deliverable for: VP of product
  Level: Context  | audience expects: Context  -> fit: OK
  Elements: 5  | mixes levels: no  | spaghetti: no
  VERDICT: COMMUNICATES

Deliverable for: new dev on the team
  Level: Container  | audience expects: Container  -> fit: OK
  Elements: 8  | mixes levels: no  | spaghetti: no
  VERDICT: COMMUNICATES

Summary: the VP gets 5 boxes with no jargon; the dev gets the city with its pieces.
Both deliverables communicate: True

Both verdicts are COMMUNICATES, and each one for the three reasons together. The Context for the VP: right level (the VP wanted the world map), a single level, 5 elements. The Container for the dev: right level (they wanted the city map), a single level, 8 elements. Neither mixes, neither is spaghetti, neither gives its audience the wrong zoom. That's the signature of a well-assembled communication package: not "one huge diagram that shows everything", but two small maps, each tuned for its reader, plus the ADR that keeps the why. Completeness in the set, communication in each piece.

Common mistakes

Delivering a single diagram "for both" (of shortcut). What happens: to save yourself work, you make an intermediate diagram and give it to the VP and the dev, hoping it serves both. It fails both: the VP gets too much, the dev too little. Why it happens: producing two maps costs more than one. How to spot it: if your package has one diagram instead of one per audience, you took the shortcut lesson 5 dismantles. How to fix it: the communication package is several pieces by design —Context for the business, Container for the devs—; the cost of the two pays for itself by not losing either of the two conversations.

Delivering the diagrams without the ADR (of forgetting the why). What happens: you give the impeccable Context and Container, but don't leave recorded why the Seller API is separated and queues. Six months later someone "simplifies" by connecting it directly to the catalog and reintroduces the risk the separation avoided. Why it happens: the diagrams feel like "the documentation" and the why is taken for granted (it is today, not in two years). How to spot it: if your package doesn't answer "why is it this way and not simpler?", you're missing the ADR. How to fix it: every structural decision someone might want to undo needs its ADR —the diagram shows the what, the ADR keeps the why—.

Validating the design and forgetting to validate the communication (of technical focus). What happens: you review a thousand times whether the architecture is correct (does the queue hold? is the token secure?) but never review whether the diagrams communicate —whether they're at their audience's level, whether they're not spaghetti—. You deliver an impeccable design no one understands. Why it happens: technical rigor feels like the "real" work and communication like an extra. How to spot it: if you didn't run your diagrams through the hygiene rules and the audience matching, you validated half the work. How to fix it: communication is part of the deliverable, not its wrapping; run the validation (level + mixing + spaghetti) over each map before deeming it good, like you did above.

Exercises

Exercise 1 — Assemble the package for a different change. Mercado now wants to add product reviews (customers rate and comment on what they bought). As architect, assemble the skeleton of the communication package: (a) what C4 level(s) do you produce and for whom?; (b) what new box(es) appear in the Container?; (c) what decision would deserve an ADR?

See solution

(a) Levels and audiences: the same base set —Context for the VP/business (who wants to see that a reviews capability is added and how it fits) and Container for the devs who'll build it—. No Component or Code needed to communicate the decision; you'd go down to Component only if the reviews service turned out to be internally complex, and only that piece.

(b) New boxes in the Container: at least a Reviews Service (or a reviews module in the Internal API, depending on the decision), and probably its storage —reviews can be stored in the existing database (PostgreSQL) or in a new table; if there's automatic moderation, maybe a queue—. In the Context, by contrast, almost nothing changes: "reviews" is an internal function, not a new actor of the world (customers already existed); the Context maybe isn't even touched, which already tells you it's more of a "city" change than a "world" one.

(c) The decision that deserves an ADR: for example, "are reviews moderated before or after being published?" —a decision with a real trade-off (moderating before protects but delays; moderating after is agile but risks bad content being visible)—. Or "is reviews a separate service or does it live inside the catalog?". Any of those deserves an ADR because it has a why with a price that someone in the future would want to understand before changing it. A change with no interesting trade-off (where a field is stored) doesn't need an ADR.

Exercise 2 — Detect the badly-assembled package. A colleague hands you their communication package for "opening the API to external vendors": a single diagram showing the Context, the six containers, the internal components of the Seller API, and the validation classes, all on one sheet with 26 elements; and no ADR. Without running code, diagnose what's wrong using the module's rules and say how you'd redo it.

See solution

Diagnosis:

  • Mixes levels: the diagram joins Context (the system and its actors), Container (the six pieces), Component (the interior of the Seller API), and Code (the validation classes) on a single sheet —four zoom levels stacked—. It fails the "one diagram, one level" rule.
  • Spaghetti: 26 elements exceed the legible limit of 20. No one follows that with the eye.
  • Missing the ADR: there's no record of why the Seller API is separated and queues; the package communicates (badly) the what and nothing of the why.
  • Audience: a single diagram can't serve the VP and the dev at once; this one, besides, serves neither —the VP drowns, the dev can't find their clean map—.

How I'd redo it: I split it into the correct package. (1) A clean Context (5-6 boxes) for the VP. (2) A clean Container (8 boxes) for the dev, with the Seller API and the queue. (3) If the Seller API turns out complex inside and the dev needs it, a Component only of that piece —separate diagram, not stacked—. The validation classes don't go in any hand-drawn diagram; they're read from the code. (4) An ADR that explains why the Seller API is separated and queues. And (5) I run each diagram through the validation (level + mixing + spaghetti) before delivering it. The 26-element monster becomes 2-3 clean maps plus an ADR: the same information, now communicated.

Exercise 3 — The delivery script. You have your package ready (Context, Container, ADR). Tomorrow you present it in a meeting where the VP of product and the new dev will be, in the same room. Write, in four or five sentences, the script of how you run the meeting to serve both audiences without drowning or losing either.

See solution

A script that works (applies the technique of sequencing levels from the common Context, from lesson 5):

  1. I open with the Context, for everyone. "Here's Mercado and its world. Today vendors publish through the web; what we're going to build is letting external vendors integrate by API. That opens us to many more vendors." (The VP already has what they need for their decision; the dev has the frame.)
  2. I frame the decision and its why, briefly. "We decided to expose that API as a separate piece that doesn't touch the catalog directly, for security; the detail of the why is in ADR-021 if you want to go deeper later." (The VP understands there's a reason; the dev knows where it's recorded.)
  3. I announce the zoom change. "VP, with this you already have the picture to approve; from here on I go down to the technical detail for [dev]. Stay if you're interested, but the business decision is already on the table."
  4. I go down to the Container, for the dev. "[Dev], these are the pieces; what you're going to build is the Seller API and its queue, here, separated from the Internal API for the reason we saw."
  5. I close. "The package —the two diagrams and the ADR— is in the repo, versioned with the code, so it doesn't go out of date."

The key: the Context is the common language where the meeting starts; from there you go down by levels announcing who each one is for, so the VP receives their world and can disconnect without feeling excluded, and the dev receives their city. No one drowns, no one gets lost.

Summary: what you built and where it goes next

In this project you assembled the complete communication package of a real Mercado change —opening the platform to external vendors by API— for two different audiences. You produced the Context for the VP (the render: the world map with the new vendors path marked, six boxes with no jargon), the Container for the new dev (the plans: the deployable pieces with the Seller API and the queue, eight boxes with technologies), and the ADR-021 that keeps the why of the decision (the notes: why the Seller API is separated and queues, with the price accepted knowingly). And you validated the package: the two maps came out COMMUNICATES because each one respects the three module rules at once —the right level for its audience, a single abstraction level, and an element count under the legible limit—. That's the real deliverable of an architect who communicates a change: not a giant document or a total diagram, but a file of small pieces, each tuned for its reader.

With this you close module 3. You now know how to communicate an architecture so it's understood: choose the C4 zoom, draw the two maps that matter most, know when to stop going down, choose the right level for each audience, use the ADR as the why that travels through time, and avoid the 500-page document and the spaghetti diagram by keeping the docs versioned with the code.

Where it goes next. Communicating well is necessary, but not sufficient. You can have the perfect Context for the VP, the perfect Container for the dev, and the impeccable ADR —and still the decision doesn't get executed, because the payments team isn't convinced, because no one has the formal authority to impose it, because the architect became the bottleneck everything must pass through—. Module 4, leading without authority, picks up right there: how the architect influences instead of commands, sustains the conversations that keep a decision alive (the load-bearing conversations), practices disagree and commit, and avoids becoming the funnel we talked about in module 1. Communication is the tool; turning it into real execution, when you don't have the power to command, is the craft of the next module.

Resources