Architecture Documentation and Communication Questions
Making an architecture legible to others: architecture decision records (templates, alternatives and consequences, lifecycle, supersession, tagging and discoverability), diagramming and visualization (C4, sequence, deployment, data-flow and trust-boundary diagrams, notation conventions), and communicating designs to technical and non-technical stakeholders. Covers capturing rationale, documenting what a system does under failure, load and consistency trade-offs, recording observability, SLO and security content, running design reviews, keeping docs and diagrams current, and presenting a system clearly under time pressure. The communication skill that separates a good design from an understood one.
What makes an architecture diagram good or bad? What conventions do you insist on for shapes, arrows, layering, legends and labelling, and what would make you send a diagram back?
Sample Answer
Direct answer
A good architecture diagram answers one question for a named audience at one level of abstraction, and a reader can understand it without the author in the room. A bad one mixes levels, has unlabeled arrows and no legend, and shows every component the author knows about. I insist on: a title stating purpose and audience, consistent shapes with a legend, arrows that say what flows and how, sensible layering, and metadata (owner, date, version). I send a diagram back when a reader would have to guess what an arrow means.
Conventions I insist on
- Shapes: one shape per kind of thing (service, data store, queue, external system), explained in a legend. Color never carries meaning alone (colour-blind and print readers).
- Arrows: labeled with a verb plus protocol ("submits order, HTTPS"); direction means "who initiates the call" and I say so on the legend.
- Sync versus async: solid line for a call the caller waits on, dashed for messages and events. Mixing them is a common source of wrong assumptions about failure.
- Layering: users at the top or left, data at the bottom or right; group things inside boundaries (zone, team, trust boundary) rather than scattering them.
- Labelling: names match the code repository and dashboards; every acronym is expanded once.
- Annotations: owner and team, a risk marker on single points of failure (SPOF, one component whose loss takes the system down), a link from each box to its runbook (the operating guide for when it breaks), and the date and version the diagram describes.
In the example below, gRPC is a fast request protocol between services, and the last node is the legend: a note box stating what solid and dashed arrows mean, deliberately connected to nothing so nobody reads it as a data flow. Note the queue-to-worker arrow points from the worker to the queue because, by the convention above, the arrow points at what is called and the worker polls the queue; if the broker pushes messages to the worker, draw it the other way and say so on the legend.
flowchart LR
U[Client] -->|POST order, HTTPS| A[API]
A -->|reserve stock, gRPC| S[Stock service]
A -.->|OrderPlaced event| Q[[Queue]]
W[Fulfilment worker] -.->|polls for events| Q
L[Legend: solid = waits for reply, dashed = event or message]
Structure views versus a sequence view These follow the C4 model (four zoom levels: Context, Container, Component, Code). A container view shows the deployable pieces (apps, services, databases); a component view zooms into the parts inside one container; both show structure, what exists and connects. Each diagram should sit at one such level of abstraction (zoom level). A sequence view (participants across the top, time flowing down) shows order and timing, so use it to show retries, timeouts and failure handling. If the question is "what happens when this fails", structure alone cannot answer it.
Worked example of sending one back
Submitted: a box "Backend" with unlabeled arrows to "DB" and "Cache", cloud icons, no legend, dated last year. My review: name the components ("Order API", "Orders database"); label each arrow; add a legend; mark the cache as a shared dependency; add the owner and a runbook link; remove the dated icons that convey nothing. After the edit a stranger can say what fails when the cache is down. The corrected diagram, as text:
[Order API] --reads/writes orders, SQL--> [Orders database]
[Order API] --reads hot items, Redis--> [Shared cache] (shared dependency, owner: platform)
Legend: solid = synchronous call. Owner: orders team. Runbook: link. Date: 2026-09-01
Send-back triggers
- Unlabeled arrows. 2. Mixed abstraction levels. 3. No legend, or unexplained icons. 4. More elements than a reader can hold, roughly fifteen or more, without grouping. 5. Undated or stale. 6. No indication of sync versus async. 7. Crossing lines that could be untangled by regrouping.
Trade-offs and pitfalls
- Strict conventions help only if the team shares them; agree on a template, not on taste.
- A beautiful diagram that is out of date is worse than an ugly one that is correct, so keep it in the repository next to the code.
How would you show data flowing between services on a diagram, and when would you prefer labelled arrows, a sequence diagram or a swimlane view?
Sample Answer
Direct answer
On a diagram I show data flow with labelled arrows: each arrow points in one stated direction (the way the data moves, or the direction of the call, chosen once and printed in the legend; see the conventions below) and carries a short label saying what data, and how it travels (sync request or async event). I switch to a sequence diagram when the order and timing of the exchange matter, and to a swimlane view when the story is about who hands work to whom across teams or systems.
Labelled arrows (the default)
Decide two conventions and print them in a legend:
- Arrow direction: either "direction of the call" or "direction of the data". Pick one. Mixing them is the most common source of confusion, because a request arrow points to the callee while the data it carries back points the other way.
- Line style: solid line for synchronous (caller waits), dashed for asynchronous (message or event, caller moves on).
Example for placing an order:
| From | To | Label | Style |
|---|---|---|---|
| Web app | order-service | "order details (JSON)" | solid |
| order-service | payment-service | "charge request" | solid |
| order-service | message queue | "OrderPlaced event" | dashed |
| queue | email-service | "OrderPlaced event" | dashed |
Which view when
| Need | Use | Why |
|---|---|---|
| Which services exchange which data, at a glance | Labelled arrows | Shows structure compactly |
| Order, timing, retries, what happens on failure | Sequence diagram | Time flows downwards, so steps are ordered and failure branches can be drawn |
| Responsibility across teams or actors, handoffs, approvals | Swimlane | One lane (row or column) per team or system; the arrow crossing a lane line is a handoff |
Example of the difference
The order flow above shows in one arrow-diagram that the email service gets an event. It cannot show that the event is sent only after the payment succeeds. A sequence diagram shows exactly that (payment first, event second). A swimlane version, with lanes "Shop team", "Payments team" and "Notifications team", shows that the shop team hands off to payments, and that notifications never talk to payments directly.
Pitfalls
- Unlabelled arrows say "these are connected" but not "what moves", which is the point of a data-flow diagram.
- Too many arrows: keep to roughly 10 arrows per diagram and split by flow instead.
- Do not put personal or sensitive data flows without marking them; a small tag such as "contains personal data" on the arrow is worth adding when the diagram will be used in a security or privacy review.
You have a list of service-dependency pairs. Write a function that produces a Graphviz DOT graph from them, and explain why you might generate diagrams from data rather than draw them by hand.
Sample Answer
Approach
Turn the pairs into a set (removing duplicates), sort nodes and edges so the output is identical on every run, and emit one node line and one edge line per item in the DOT language (Graphviz's plain-text graph format: a -> b; means a directed edge). Escape quotes so an odd service name cannot break the file. The same structured data also produces a markdown summary table, so the diagram and the documentation come from one source.
Code (save as archdocs.py)
import json
import sys
def esc(text):
return text.replace("\\", "\\\\").replace('"', '\\"')
def to_dot(pairs, owners=None):
owners = owners or {}
edges = sorted(set(pairs)) # dedupe + stable order
nodes = sorted({n for pair in edges for n in pair} | set(owners))
lines = ["digraph services {", " rankdir=LR;", " node [shape=box];"]
for n in nodes:
label = esc(n) + ("\\n(" + esc(owners[n]) + ")" if n in owners else "")
lines.append(f' "{esc(n)}" [label="{label}"];')
for src, dst in edges:
lines.append(f' "{esc(src)}" -> "{esc(dst)}";')
lines.append("}")
return "\n".join(lines)
def to_markdown(services):
rows = ["| Service | Owner | SLO | Diagram |", "|---|---|---|---|"]
for name in sorted(services):
s = services[name]
rows.append(f"| {name} | {s['owner']} | {s['slo']} | [{s['diagram']}]({s['diagram']}) |")
return "\n".join(rows)
if __name__ == "__main__":
mode, path = sys.argv[1], sys.argv[2]
with open(path) as f:
data = json.load(f)
if mode == "dot":
owners = {n: s["owner"] for n, s in data["services"].items()}
print(to_dot([tuple(e) for e in data["edges"]], owners))
else:
print(to_markdown(data["services"]))
Sample input, saved as services.json, then two runs:
cat > services.json <<'EOF'
{
"edges": [["web", "auth"], ["web", "catalog"], ["catalog", "db"], ["auth", "db"], ["web", "auth"]],
"services": {
"auth": {"owner": "identity-team", "slo": "99.9% login success", "diagram": "docs/auth.svg"},
"catalog": {"owner": "catalog-team", "slo": "p95 < 300 ms", "diagram": "docs/catalog.svg"}
}
}
EOF
python3 archdocs.py dot services.json
python3 archdocs.py table services.json
Output of the first run (the duplicate web -> auth pair appears once):
digraph services {
rankdir=LR;
node [shape=box];
"auth" [label="auth\n(identity-team)"];
"catalog" [label="catalog\n(catalog-team)"];
"db" [label="db"];
"web" [label="web"];
"auth" -> "db";
"catalog" -> "db";
"web" -> "auth";
"web" -> "catalog";
}
Output of the second run:
| Service | Owner | SLO | Diagram |
|---|---|---|---|
| auth | identity-team | 99.9% login success | [docs/auth.svg](docs/auth.svg) |
| catalog | catalog-team | p95 < 300 ms | [docs/catalog.svg](docs/catalog.svg) |
Reading the code line by line
escputs a backslash before\and", so a name likea"bcannot end the quoted string early.edges = sorted(set(pairs)): thesetdrops the duplicate("web", "auth"), andsortedmakes the order the same on every run.nodes = sorted({n for pair in edges for n in pair} | set(owners)): a set comprehension that loops over each pair and then each name in it, so every service is listed once even if it appears in many pairs; the| set(owners)adds services that have an owner but no dependency edges, which would otherwise vanish from the picture (an isolated service is exactly the kind of gap a landscape diagram should show).rankdir=LR;tells Graphviz to lay the graph out left to right (the default is top to bottom);node [shape=box];makes every node a rectangle unless overridden.- The node loop writes one line per service, adding the owner on a second label line (
\nis a line break inside a DOT label) when one is known; the edge loop writes"src" -> "dst";. to_markdownbuilds one table row per service from the same data; SLO means service-level objective, the reliability target the team commits to (for example "p95 < 300 ms", meaning 95 percent of requests finish under 300 milliseconds).
Pipe the DOT output into dot -Tsvg (from the Graphviz tools) to get an image, or paste it into any online Graphviz viewer.
Why generate diagrams from data instead of drawing them
- Single source of truth: the dependency list already lives in a service catalog or config. A diagram built from it cannot disagree with it.
- Stays current: regenerate on every merge in continuous integration (CI, the automated build), so the picture never lags behind the system.
- Reviewable: a change to the dependency list appears as a small text diff in a pull request, which a screenshot of a hand-drawn box cannot offer.
- Consistent: every diagram gets the same shapes and names, which removes the arguments about icons.
- Cost of hand-drawing: nicer layout and emphasis. I would still hand-draw the executive overview and generate the detailed dependency views.
Complexity
Building the set is O(E) for E pairs and sorting is O(E log E) (nodes are at most 2E plus the number V of owned services), so the cost is O((E + V) log (E + V)), with O(E + V) memory for the output.
Edge cases
- Duplicate pairs are removed by the set.
- Names with quotes or backslashes are escaped (
to_dot([('a"b', 'c')])yields the node"a\"b"). - A service with no owner in the data gets no owner line, which is itself a useful documentation gap to flag.
- A service that is in the service data but appears in no pair is still drawn, as an unconnected box, because its name comes from the owners map.
- Cycles are valid in DOT and drawn as-is, which is helpful because circular dependencies are worth seeing.
- Very large graphs (hundreds of nodes) become unreadable: filter to one domain, or one hop from a chosen service, before drawing.
You are writing an ADR that both engineers and non-technical stakeholders will read. How do you decide the level of technical detail, and how would you summarise the impact of the change on cost, maintainability, security and operations so both audiences can act on it?
Sample Answer
Direct answer
I write the ADR (Architecture Decision Record) in layers: a plain-language summary on top that a non-technical stakeholder can act on, the engineering reasoning in the body, and deep technical detail linked out. I decide the detail level with one test: include only what a reader needs in order to judge the trade-off or act on the decision. The impact on cost, maintainability, security and operations goes in a small table that both audiences read.
Deciding the level of detail
- Ask of each detail: does it change what the reader would decide or do? If not, link it out (configuration values, protocol internals, benchmarks).
- Explain unavoidable jargon once in a phrase, or replace it ("a queue: a waiting line that lets one part hand work to another").
- The summary test: could a non-technical reader repeat back what is being decided, what it costs, and what we need from them?
Impact summary table
Each row says what changes and who feels it (Impact), how big (Size), and what we are doing about it (Mitigation). Label estimates as estimates.
Worked example (illustrative): moving report generation from a nightly script on one virtual machine to a queue plus small workers
Summary for decision makers: "Reports currently stop if one server restarts. We will move to a waiting-line design so failed reports retry on their own. Running cost rises by about 70 dollars a month (roughly 840 a year, an estimate). We need approval for that extra spend by the end of the month."
| Area | Impact | Size | Mitigation |
|---|---|---|---|
| Cost | Today: one server at an assumed 140 dollars a month. After: queue plus two workers at an assumed 210 dollars a month | +70 a month, so 840 a year (70 x 12), estimate | Review after one quarter of real bills |
| Maintainability | One long script becomes two small workers; one new part (the queue) to learn | Moderate learning cost, less risk per change | Pair on the first worker; write a runbook (step-by-step instructions for handling an alert) |
| Security | Workers get narrower permissions and no shared credentials file; one more component to patch | Net improvement, small new surface | Automated patching, scoped access |
| Operations | Reports no longer fail silently on restart; on-call (the engineer who responds to alerts) gains a queue-depth alarm | Fewer missed reports, one new alert | Alarm threshold and runbook before launch |
The body then gives engineers the options considered (managed queue, cron on two servers), the failure-handling design, and links to the deployment details.
Trade-offs and pitfalls
- One text for both audiences: it ends up too vague for engineers and too dense for executives. Layering serves both.
- Hiding the downside: stakeholders lose trust the first time a hidden cost appears. Lead with the price.
- Unlabelled numbers: mark assumptions and estimates so nobody treats them as commitments.
- Burying the ask: state what you need (approval, funding, a date) in the first three lines.
What security and authentication content must an architecture document for a public API contain, and how do you present it to a security reviewer versus a developer?
Sample Answer
Direct answer
The document must state, for a public API, who can call it, how callers prove who they are, what they are allowed to do, how data is protected in transit and at rest, and how abuse is limited and detected. A security reviewer wants threats, controls and evidence; a developer wants how to authenticate correctly and what errors to expect. I would write one source of truth and give each reader a different entry point into it.
Required content
- Authentication: the mechanism (for example OAuth 2.0, the standard for delegated access, with short-lived access tokens (an access token is a temporary string the caller sends with each request to prove it was authorised), or API keys for server-to-server use), where tokens are issued, lifetime, refresh and revocation.
- Authorization: the model (scopes are named permissions carried on the token, such as
orders:read; roles group permissions by job) and where it is enforced (gateway versus service), including object-level checks (a caller must own the record it requests: if user A asks for/orders/1002and that order belongs to user B, the API must refuse, even though A's token is valid; broken object-level authorization is the top risk in the OWASP API Security list, OWASP being an open security-community project). - Trust boundaries and data flow: what is exposed publicly, what sits behind the gateway, and where TLS ends (TLS is the encryption that protects traffic on the network; "where it ends" is the point where traffic is decrypted and must be trusted from then on).
- Data protection: encryption in transit and at rest, classification of fields, what is never logged.
- Abuse controls: rate limits, quotas, input validation, size limits, and how bad clients are blocked.
- Threat model: a threat model is a structured list of what could go wrong and who might attack, each threat paired with the control that answers it. Record the residual risk (what remains after the controls) and the named person who accepts it.
- Secrets and key management, audit logging, incident contacts.
Presenting to each reader
| Security reviewer | Developer | |
|---|---|---|
| Opens with | Threat table and trust-boundary diagram | "How to get a token" with a copyable request |
| Format | Threat, control, evidence (config link, test, scan result), residual risk | Step-by-step, examples, error codes, scope table |
| Cares about | Gaps, assumptions, compliance mapping (which control satisfies which regulation or standard requirement) | Time to a working, correct call |
Worked example (illustrative fragment)
Security reviewer fragment, threat table rows:
| Threat | Control | Evidence | Residual risk |
|---|---|---|---|
| Stolen access token replayed | 15-minute lifetime; sender-bound refresh tokens (a refresh token that only the client it was issued to can use, so a copy is useless); revocation list at the gateway (token ids refused even before they expire) | Gateway policy file, revocation test | Up to 15 minutes of misuse, accepted by the security owner |
| User A reads user B's order by changing the id in the URL | Service checks the order's owner equals the caller on every read | Automated test that requests another user's order and expects a refusal | None known; retested each release |
Developer fragment, the "how to get a token" page (client-credentials grant means a server application exchanges its own id and secret for a token, with no human user involved; a bearer token is one that works for whoever holds it, so it must be sent only over TLS):
POST /oauth/token
grant_type=client_credentials&client_id=<your-id>&client_secret=<your-secret>&scope=orders:read
-> {"access_token": "<token>", "expires_in": 900}
GET /orders/1002
Authorization: Bearer <token>
Errors: 401 means the API does not know who you are (no token, malformed or expired), so fetch a new token. 403 means it knows who you are but you may not do this (the token lacks the scope, or the order is not yours), so requesting a new token will not help. That difference is authentication (proving who you are) versus authorization (what you are allowed to do).
Pitfalls: describing the design without evidence a reviewer can check; documenting the mechanism but not the failure responses; putting secrets or real tokens in examples; letting the doc and the gateway configuration diverge. Generate the developer reference from the API's OpenAPI description (a machine-readable file listing every endpoint, parameter and response) where you can, so it cannot drift.
Unlock Full Question Bank
Get access to all Architecture Documentation and Communication interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.