Skip to content
/ Michaël Hompus

Chapter 3 draws the boundary of your system. If it is unclear what is inside and outside, integrations and expectations will break first. In this article I show what belongs in chapter 3, what to keep out, and a minimal structure you can copy, plus a small example from Pitstop.

Chapter 3 is the last chapter in the “Why and where” group. It is where you draw the line between your system and the outside world.

If that line is unclear, failures show up eventually: unclear responsibilities, failing integrations, and mismatched expectations.

This chapter is split into two views:

  • Business context: who interacts with the system, and what value or information is exchanged.
  • Technical context: what interfaces exist, and how integration actually happens.

What belongs in chapter 3 (and what does not)

Chapter 3 of an arc42 document answers one question:

What is inside our system, what is outside, and how do we interact?

What belongs here:

  • A clear inside vs outside boundary.
  • External business actors and neighboring systems with responsibilities.
  • The direction of exchanges (who initiates, who responds).
  • Examples of value or data exchanged (not only APIs: files, emails, manual exports, spreadsheets, SFTP drops).
  • The most important interfaces (APIs, messaging, files, UI hand-offs, SSO, batch jobs).
  • Links to existing interface documentation (OpenAPI/AsyncAPI/specs) if it exists.

What does not belong here:

  • Internal building blocks and components (chapter 5).
  • Runtime scenarios and sequencing (chapter 6).
  • Deployment layouts (chapter 7).
  • Technical design details that do not cross the boundary.

Note

If there is no separate API documentation, chapter 3 is the place to document your interfaces. Even when you can link to OpenAPI or AsyncAPI documents, include 1–2 small sample payloads here.
It makes the integration real and readable without forcing people to open a separate spec.

Diagrams: modeling tool or text-based

Diagrams make chapter 3 click. You can draw them in a modeling tool like Sparx Enterprise Architect, but you can also keep them close to the code using text-based diagrams.

I prefer PlantUML component diagrams for this chapter, because the system boundary and interfaces are easy to read.

Text-based diagrams work well because they are easy to diff, review, and version together with the documentation.

Tip

Use whatever keeps the diagram maintained.
A perfect diagram that nobody updates is less useful than a simple one that stays correct.

The minimum viable version

If you are short on time, aim for this:

  1. One business context diagram (or a table) listing the key actors/systems and what they exchange.
  2. One technical context diagram (or a table) listing the top interfaces with direction and protocol.
  3. For the top 1–3 interfaces: add a short example (sample payload, file format snippet, or message shape).

That is enough to prevent most boundary and integration surprises.

When this chapter becomes high value

As soon as you have quality goals around availability, latency, or operational continuity, your interfaces need a bit more than we call API X.

For your top interfaces, add:

  • SLA/SLO expectations and support windows
  • Failure behavior and fallback procedures
  • Retry/idempotency rules
  • Rate limits and quotas
  • Security and trust boundaries

This is often where the conversation moves from it depends to concrete trade-offs.

Copy/paste structure (Markdown skeleton)

Use this as a starting point and keep it small.

03-context-and-scope.md
## 3. Context and scope
### 3.1 Business context
<Who is outside the system, what value/data is exchanged?>
```plantuml
@startuml
title Business context (placeholder)
actor "External Actor" as Actor
component "Neighboring System" as Neighbor
component "Our System" as OurSystem
Actor -> OurSystem : value / info
OurSystem --> Neighbor : value / info
Neighbor --> OurSystem : value / info
@enduml
```
| External actor / system | Responsibility | Exchange with our system |
| :---------------------- | :------------- | :----------------------- |
| ... | ... | ... |
### 3.2 Technical context
<Which technical interfaces exist, and how do we integrate?>
```plantuml
@startuml
title Technical context (placeholder)
component "External System" as External
component "Identity Provider" as IdP
component "Our System" as OurSystem
External <--> OurSystem : HTTP + JSON
OurSystem --> IdP : OIDC/OAuth2
@enduml
```
| Peer | Interface | Owner | Direction | Protocol / format | Notes |
| :--- | :-------- | :---- | :-------- | :---------------- | :---- |
| ... | ... | ... | ... | ... | ... |
#### 3.2.1 Interface: <name>
**Purpose:** <why does this interface exist?>
**Direction:** <A -> B, source of truth?>
**Link to spec:** <OpenAPI/AsyncAPI link if it exists>
**Examples (recommended):**
<Description>
```json
{ "example": "payload" }
```
**Expectations and exceptions (optional):**
| SLA/SLO | Failure behavior | Retry / idempotency | Fallback / manual procedure |
| :------ | :--------------- | :------------------ | :-------------------------- |
| ... | ... | ... | ... |

Example (Pitstop)

Pitstop is my small demo system for this series. It is intentionally simple, so the documentation stays shareable.

This is what chapter 3 looks like when filled in.

3. Context and scope

3.1 Business context

Pitstop sits between planning and the workshop. It keeps work orders and status in sync so people stop copying information between tools.

Business context
Actor/SystemResponsibilityExchanges with Pitstop
CustomerBrings car, receives updatesETA updates (via advisor/portal)
Service AdvisorManages appointment & expectationsPriority changes, notes, customer communication
Workshop ForemanOrchestrates executionAssignments, reprioritization
MechanicPerforms workStatus updates, findings, time spent
Planning ServiceOwns schedule/time slotsAppointment import, reschedule suggestions
Notification Service (optional)Contact customersSMS/email updates

3.2 Technical context

Technical context
PeerInterfaceOwnerDirectionProtocol/FormatNotes
Planning ServiceAppointments APIPlanning vendorInboundREST/JSONFull import + incremental sync
Planning Service (optional)WebhooksPlanning vendorInboundHTTP/JSONPush appointment changes
Planning ServiceStatus updatesPitstop teamOutboundREST/JSONDelay, ready, reschedule proposal
Admin Overview UIWork Orders APIPitstop teamBidirectionalHTTPS/JSONRBAC, dashboards
Workshop View UILive UpdatesPitstop teamBidirectionalWebSocket/JSONLow latency, optimized payloads
Notification Service (optional)Notifications APIPitstop teamOutboundREST/JSONCustomer updates

3.2.1 Interface: Appointments API

3.2.1.1 Examples

Appointment imported from planning:

{
"appointmentId": "A-10293",
"plate": "12-AB-34",
"start": "2026-01-12T09:00:00+01:00",
"service": "OilChange",
"customerRef": "C-4451"
}

3.2.2 Interface: Work Orders API

3.2.2.1 Examples

Workshop update from a mechanic:

{
"workOrderId": "WO-7781",
"status": "WaitingForParts",
"note": "Brake pads not in stock",
"updatedBy": "mechanic-17",
"updatedAt": "2026-01-12T10:41:00+01:00"
}

To browse the full Pitstop arc42 sample, see my GitHub Gist.

Note

Interfaces are not always APIs.
Manual exports, emailed files, spreadsheets, SFTP drops, and someone retypes it are also integrations.
If information crosses the boundary, document it here.

Common mistakes I see (and made myself)

  1. Only naming neighbors, without exchanges
    A box called CRM is not useful by itself. Document what is exchanged and why.

  2. Mixing business and technical context
    Business context is about responsibilities and value. Technical context is about protocols and integration mechanics. Mixing them makes both harder to read.

  3. Treating REST as the only interface
    REST is common, but not universal. File transfers, messaging, batch jobs, manual steps and spreadsheets all matter.

  4. No ownership
    If you do not document who owns an external interface, you will not know who to call when it breaks.

  5. No direction
    We integrate with X is vague. Who initiates, who is the source of truth, who is allowed to change state?

  6. No examples
    Even when you have OpenAPI or AsyncAPI, one small payload example prevents a lot of misunderstandings.

  7. No expectations for critical interfaces
    If availability or latency matters, document assumptions: SLAs, failure behavior, retry rules, and fallback procedures.

Note

If an external stakeholder cannot recognize their responsibilities and expectations in this chapter, the integration is not documented clearly enough yet.

Done-when checklist

🔲 The system boundary is clear (inside vs outside).
🔲 Business actors and neighboring systems are listed with responsibilities.
🔲 The top interfaces are listed with direction, protocol/format, and owner.
🔲 The most important integrations have 1–3 small examples (payload, file snippet, message shape).
🔲 Critical interfaces have expectations (SLA/failure behavior) or are explicitly marked as unknown.

Next improvements backlog

  • Add links to OpenAPI/AsyncAPI documents (or create them if missing).
  • Add SLA/SLO and failure behavior for the top 3 interfaces.
  • Add a short note about trust boundaries and data classification (if relevant).
  • Add examples for non-API integrations (file drop, manual export, batch job) if they exist.
  • Review chapter 3 with external stakeholders (planning vendor/team, ops, security) for recognition and correctness.

Wrap-up

Chapter 3 is where you make expectations explicit. Most problems show up at the boundary first, so investing in this chapter pays off quickly.

This concludes the “Why and where” group of arc42 chapters. Next, we move on to the “How is it built and how does it work” group.

Next up: arc42 chapter 4, “Solution strategy”, where we describe the approach and major decisions that guide the design.

/ Michaël Hompus

Chapter 2 lists the non-negotiables that shape your design space. If you do not write these down early, they will still exist, but they will surprise you later. In this article I show what belongs in chapter 2, what to keep out, and a minimal structure you can copy, plus a small example from Pitstop.

Chapter 2 is part of the “Why and where” group. It is the chapter where you write down the rules you cannot break.

This is not about what you prefer. It is about what your organization, environment, or stakeholders already decided for you.

If you do not document constraints early, they still shape the architecture. You just discover them at the worst possible time.

Constraints also have a positive side: there are thousands of ways to build the same functionality. A short list of non-negotiables helps you narrow down options early, before you invest in the wrong direction.

I have seen teams pick a public cloud technology because it fit the solution, while the product had to run air-gapped on-premises. Or because it was “hot” (call it: conference-driven design), while operations would only support a single platform. Money got wasted before someone finally said: this was never negotiable.

What belongs in chapter 2 (and what does not)

Chapter 2 of an arc42 document answers one question:

What limits our freedom, no matter what solution we pick?

What belongs here:

  • Organizational constraints (budget/time, team skills, governance, contracting).
  • Technical constraints (platforms/stack, operations model, security/compliance rules).
  • Integration constraints (what you must connect to, formats you must accept).
  • Conventions (coding standards, CI/CD rules, naming/versioning, documentation rules).
  • References to standards you must follow.

What does not belong here:

  • Architecture choices you still get to make (save those for chapter 4 and chapter 9).
  • Personal preferences (I like microservices, we always use Kafka).
  • Detailed design, diagrams, protocols, or deployment layouts.

Note

A constraint is a rule you must follow.
A decision is a choice you make.
If you mix them, chapter 2 becomes a debate instead of a boundary.

Constraints exist on multiple levels

Organizations often have architecture and constraints at multiple levels (enterprise, domain, platform, product, application). You can use arc42 at all those levels, but in practice most teams start at the bottom: an application or service.

That is also where chapter 2 becomes very practical: many constraints already exist as company policies and standards.

Tip

Link to existing policies instead of rewriting them.
They tend to be stable, owned, and updated in one place.
Your chapter 2 should explain the impact, not duplicate the policy text.

Many policies ultimately follow from a company mission and vision. So even if a constraint looks “technical”, it often exists for a business reason. Writing down the rationale helps prevent this is stupid discussions later.

The minimum viable version

If you are short on time, aim for this:

  • 8–15 constraints in a table
  • each constraint includes:
    • a clear statement
    • a type (organizational, technical, convention, integration, compliance)
    • a short rationale
    • the impact on design
    • a reference or owner (where it came from)

That is enough to prevent surprise constraints late and to make later decisions faster.

Copy/paste structure (Markdown skeleton)

Use this as a starting point.

02-architecture-constraints.md
## 2. Architecture constraints
Non-negotiables that shape the design space.
| Constraint | Type | Rationale | Impact on design | Reference |
| :--------- | :------------- | :-------- | :--------------- | :-------- |
| ... | Organizational | ... | ... | ... |
| ... | Technical | ... | ... | ... |
| ... | Convention | ... | ... | ... |
Notes:
- If a constraint has exceptions, describe the exception path.
- Link to standards, policies, or owners as references.

Note

A table is not mandatory.
If your constraints list grows, grouping them by type (e.g., organizational, technical, conventions, compliance) can be more readable.
The key is still the same: statement, rationale, impact, and source.

Example (Pitstop)

Pitstop is my small demo system for this series. It is intentionally simple, so the documentation stays shareable.

This is what chapter 2 looks like when filled in.

2. Architecture constraints

Non-negotiables that shape the design space:

ConstraintTypeRationaleImpact on design
Must integrate with Planning Service(s)IntegrationExisting ecosystem realityAPI contracts, sync strategy, mapping rules
Near real-time UI updatesUX/OperationalWorkshop coordinationPush updates (WebSocket/SSE) or efficient polling
Degraded-mode operationOperationalGarage networks can be unreliableLocal cache/queue, retry, conflict handling
Containerized deploymentPlatformStandard ops modelRegistry, base images, runtime policy
Automated CI + testsProcessFast feedback and reliabilityPipeline ownership + test environments
GDPR / minimal personal dataComplianceCustomer dataData minimization, retention rules, audit controls
Deviations recorded as ADRsGovernancePrevent silent divergenceADR workflow and traceability (chapter 9)

To browse the full Pitstop arc42 sample, see my GitHub Gist.

Common mistakes I see (and made myself)

  1. Writing constraints too late
    If chapter 2 is empty, people will assume freedom that does not exist. Then the first real constraint shows up during implementation, procurement, or security review.

  2. Using vague words
    Secure, fast, cloud-ready are not constraints.
    Write constraints as rules you can test against: must run on-prem, must be air-gapped, must use SSO.

  3. Mixing constraints and decisions
    We will use PostgreSQL is usually a decision.
    We must use the company-managed PostgreSQL platform is a constraint.
    If it is not truly non-negotiable, move it to chapter 4 or chapter 9.

  4. No impact column
    A constraint without impact does not help the team. The value is in translating a rule into a design consequence.

  5. Forgetting conventions and governance
    Conventions feel boring until they break delivery: CI/CD rules, versioning, naming, documentation rules, ADR requirements. Put them here so they are explicit.

Exceptions and experiments

Non-negotiable does not mean “never”. Sometimes you run an experiment to learn, or you need an exception for a specific case.

Tip

When you make an exception, document it as an ADR and link it here.
The goal is not bureaucracy.
The goal is that the next team does not rediscover the same debate.

Done-when checklist

🔲 Chapter 2 contains the real non-negotiables, not preferences.
🔲 Each constraint has a clear impact on design and delivery.
🔲 Each constraint has a source (owner, standard, policy, or link).
🔲 The list is short enough to scan, but complete enough to prevent surprises.

Next improvements backlog

  • Review the list with ops, security, and the product owner (fast reality check).
  • Add links to central standards (security baseline, platform rules, CI/CD guidance).
  • Mark constraints that are assumptions and confirm them (or remove them).
  • Add ADR links for any local deviations from central architecture/platform rules.
  • Split the table into sub-sections if it grows (organizational, technical, conventions).

Wrap-up

Chapter 2 is where you protect your future self. Constraints narrow the solution space, so later decisions become faster and more consistent.

Next up: arc42 chapter 3, “Context and scope”, where we draw the boundary and make integrations and expectations explicit.

/ Michaël Hompus

Chapter 1 sets the direction for the entire architecture document. If you do not know why you are building this and who it is for, you cannot design it properly. In this article I show what belongs in chapter 1, what to keep out, and a minimal structure you can copy, plus a small example from Pitstop.

Chapter 1 is part of the “Why and where” group. The audience for this chapter is everyone involved in the project. Even nontechnical stakeholders should read and understand it.

It is the chapter that can prevent a lot of confusion later. You lay the foundation for everything that follows. Not by adding too much detail (there are other chapters for that), but by making the intent explicit.

If you do not know why you are building this application and who it is for, you and your team cannot design it properly.

What belongs in chapter 1 (and what does not)

Chapter 1 of an arc42 document answers the “why” and “for whom” questions, without going into design.

What belongs here:

  • A short problem statement and what you are building.
  • The most important requirements (and explicit non-goals).
  • The top quality goals that will drive trade-offs later.
  • The key stakeholders and what they care about.

What does not belong here:

  • Component diagrams, deployments, protocols, and technical choices (save those for later chapters).
  • A complete requirements catalog (link to it if it exists).
  • Long background stories and project history.

Note

If you can only get one chapter right, get chapter 1 right. Maybe quite obvious, but it is the chapter everyone will read first.

The minimum viable version

If you are short on time, aim for this:

  1. One paragraph: what is the system and why does it exist?
  2. 5–10 bullets: the most important requirements.
  3. 3–5 quality goals: short and measurable.
  4. A small stakeholder table.

That is enough to align a team and reduce surprises.

Tip

Chapter 1 is also a great place to add something recognizable, like a small logo or cover image. It helps people quickly confirm they are reading the right document. If you do not have a logo, an LLM image generator can help you create one quickly.

Copy/paste structure (Markdown skeleton)

Use this as a starting point and keep it small.

01-introduction-and-goals.md
## 1. Introduction and goals
<1–3 short paragraphs: what are we building, why now, what pain does it solve?>
### 1.1 Requirements overview
The most important requirements:
- ...
- ...
Explicit non-goals:
- ...
- ...
### 1.2 Quality goals
Top quality goals (measurable):
| Priority | Quality | Scenario (short) | Acceptance criteria (example) |
| -------: | :------ | :--------------- | :---------------------------- |
| 1 | ... | ... | ... |
### 1.3 Stakeholders
| Stakeholder | Expectations |
| :---------- | :----------- |
| ... | ... |

Example (Pitstop)

Pitstop is my small demo system for this series. It is intentionally simple, so the documentation stays shareable.

This is what chapter 1 looks like when filled in.

1. Introduction and goals

Garages struggle to keep planning and workshop execution in sync. Most garages use a planning tool for appointments and a separate admin/workshop system for execution. When jobs change (delay, extra work, parts missing), updates are handled manually.

Pitstop solves this by providing a single operational source of truth for work orders and status, and synchronizing planning and workshop execution.

1.1 Requirements overview

  • Import appointments from one or more planning services.
  • Convert appointments into work orders (jobs/tasks, estimates, required skills, bay assignment).
  • Provide an admin overview (today’s workload, lateness, bay utilization, priorities).
  • Provide a workshop view (per bay/technician task list with fast status updates and notes).
  • Push status changes back to planning (delays, ready-for-pickup, reschedule proposals).

Explicit non-goals:

  • Pitstop is not the planning product.
  • Pitstop is not inventory management.
  • Pitstop is not billing/accounting.

1.2 Quality goals

PriorityQualityScenario (short)Acceptance criteria (example)
1ConsistencyAdmin + Workshop must show the same job stateStatus updates visible in all UIs within <= 2 seconds under normal connectivity
2ResilienceWorkshop continues during flaky internetDegraded mode works; updates sync when online
3ModifiabilityAdd a new planning integrationNew integration in <= 2 days for a typical planning REST API without changing core logic

1.3 Stakeholders

StakeholderExpectations
Garage Owner / ManagerThroughput, predictable planning, fewer no-shows, visibility
Service Advisor (front desk)Reliable customer promises, quick rescheduling
Workshop ForemanClear priorities, balanced bays, fewer interruptions
MechanicsSimple task list, fast updates, less admin burden

To browse the full Pitstop arc42 sample, see my GitHub Gist.

Common mistakes I see (and made myself)

  1. Only the name of the application
    If chapter 1 starts with just System X and a few bullets, it does not help anyone. Add 2–3 sentences that set the scene: what problem exists today, who feels the pain, and why building this is worth it.

  2. Listing features instead of goals
    Features are implementation ideas. Goals are outcomes. If you can explain the outcome, the team can still choose the best solution later.

  3. No explicit non-goals
    Non-goals prevent scope creep and wrong expectations. If something is out of scope, say so early, and say why.

  4. No quality goals (or only vague ones)
    If you do not write down quality goals, every trade-off later becomes a debate with no shared reference. The hard part is that stakeholders often do not have a list.

    A practical approach that works well:

    • Ask what would make this a success and what would make people complain.
    • Turn the answers into 3–5 short scenarios with one measurable criterion each.
    • Start with rough numbers. You can refine them later once you have usage data.
  5. Stakeholders = the team or product owner
    The development team is not the only stakeholder. Everyone interacting with the system (directly or indirectly) is a stakeholder. If you require something from them, or they expect a service from your system, include them.

    A good way to expand the list:

    • End users (different roles, not one bucket)
    • Neighboring systems and their owners
    • Operations and support
    • Security, compliance, and governance
    • Business owners and managers

Done-when checklist

🔲 Chapter 1 fits on a few screens.
🔲 A new team member can explain the system after reading it.
🔲 Non-goals are explicit.
🔲 There are 3–5 quality goals with at least one measurable criterion each.
🔲 Stakeholders are mapped to expectations, not just listed.

Next improvements backlog

  • Add links to any existing requirement sources (backlog items, product brief, etc.).
  • Refine acceptance criteria based on observed production behavior.
  • Split stakeholders into “users” and “neighbors” if the list grows.
  • Add a short glossary entry in chapter 12 for any domain terms used in chapter 1.

Wrap-up

Chapter 1 is the compass. 🧭
It does not describe the architecture, it explains what the architecture must achieve.

Next up: chapter 2, “Architecture constraints”, where we write down the rules that limit our freedom, before they surprise us later.

/ Michaël Hompus

After my "The Art of Simple Software Architecture Documentation" talk, a surprising number of people asked for the slides because they saw the deck as a reference guide. This post is the starting point: why arc42 works so well, how I approach it in practice, and how this series will grow over time, without pretending it is finished on day one.

After my talk “Arc42: The Art of Simple Software Architecture Documentation” at Bitbash 2026, a surprising number of people reached out.

Not just with nice talk, but with can you share the slides?. People told me they saw the deck as a reference guide.

That was the trigger to write it down properly.

When the same question comes back again and again, it usually means the same thing: arc42 is widely known, but it is hard to find practical guidance and shareable examples.

Tip

I will be giving this talk March 11 at Future Tech 2026 in Utrecht 🇳🇱.
If you like this topic and want the full story live, come say hi.

Future Tech 2026 banner featuring Michaël Hompus and the session title "arc42: The Art of Simple Software Architecture Documentation"

What this series is (and what it is not)

This is a series of short, practical posts about using arc42 to document software architecture.

Not “architecture theory”.
Not “how to draw 17 diagrams per sprint”.
And definitely not “write a document once and forget it”.

The goal is simple:

Help you start small, document what matters, and keep improving over time.

arc42 is great because it gives structure without forcing you into a heavyweight process. But that flexibility also creates a common problem:

  • everyone agrees the template looks nice,
  • fewer people agree on what good content looks like,
  • and almost nobody can share real examples because most architecture docs live behind company firewalls.

So I am going to do two things:

  1. Explain how I fill each arc42 chapter in practice (with guidance, pitfalls, and “done-when” checks).
  2. Use a small demo system (Pitstop) as a shareable example, so it does not stay abstract.

Why arc42?

arc42 works well because it pushes you to answer the questions that always come back later:

  • What are we building and why?
  • What must be true about quality (performance, security, maintainability, etc.)?
  • What are the constraints that narrow our solution space?
  • How is it structured?
  • How does it run?
  • How is it deployed?
  • What decisions did we make, and why?

It is not perfect. But it is a really good default.

And defaults matter, because no documentation approach survives contact with we have sprint goals.

The “minimal but honest” rule

This is the rule I try to follow:

Write the smallest amount of documentation that prevents expensive misunderstandings.

That means:

  • do not write marketing material
  • do not write a novel or fill a bookcase
  • do not invent details you do not know yet
  • and do not pretend you will get it right on the first try

Architecture docs are not a deliverable. They are a feedback loop.

Every time you revisit a document with fresh eyes, you will spot improvements. That is not failure, that is the whole point.

The arc42 chapters

One thing I like about arc42 is that the 12 chapters are not a random list. They can be grouped into a few themes, which makes it easier to navigate and to explain to your team.

This is the same grouping I used in my slides.

Why and where

These chapters set the direction and boundaries.

  1. Introduction and goals
  2. Architecture constraints
  3. Context and scope

How is it built and how does it work

These chapters describe the actual solution.

  1. Solution strategy
  2. Building block view
  3. Runtime view

Rules, decisions, and qualities

These chapters keep the system consistent and explain trade-offs.

  1. Deployment view
  2. Cross-cutting concepts
  3. Architecture decisions
  4. Quality

Reality and shared language

These chapters make the documentation useful in real life, long after the first design.

  1. Risks and technical debt
  2. Glossary

If you have only ever used arc42 as a template, try this grouping with your team. It tends to make the “why does this chapter exist” discussion much easier.

How the posts will be structured

Each chapter post will follow the same pattern:

  • What belongs in this chapter (and what does not)
  • The minimum viable version (the smallest useful content)
  • A copy/paste structure (Markdown skeleton)
  • Example from Pitstop (small excerpt, no hand-wavy lorem ipsum)
  • Common mistakes I see
  • Done-when checklist
  • Next improvements backlog (because iteration is real)

That way you can skim when you are busy, or go deeper when you are implementing it.

Where to start (today)

I am not publishing the entire series at once. This post is the hub, and I will add links here as each article is published.

If you only write 3 things, write these first:

If you only write 3 things, write these

  1. Chapter 1: Introduction and goals
    If you do not know the goals and stakeholders, you cannot make good design trade-offs.

  2. Chapter 2: Architecture constraints
    Hidden constraints are the source of late-stage surprise and pain, but can also help narrow down options early.

  3. Chapter 3: Context and scope
    If the boundaries are unclear, integrations and expectations will break first.

Once those are clear, you can add:

  • a rough building block view,
  • one or two runtime scenarios,
  • and you have already prevented a lot of chaos.

Series status

This hub will be updated as posts go live:

Planned posts

Why & where:

How is it built & how does it work:

  • 4. Solution strategy
  • 5. Building block view
  • 6. Runtime view

Rules, decisions, & qualities:

  • 7. Deployment view
  • 8. Cross-cutting concepts
  • 9. Architecture decisions
  • 10. Quality

Reality & shared language:

  • 11. Risks and technical debt
  • 12. Glossary

You will also find all posts in the series navigation in the sidebar.

The promise (so you do not get disappointed)

I am not going to claim this will be “the definitive arc42 guide”.

It will be:

  • a practical reference,
  • based on real usage,
  • improved as I use it more (and as people send feedback).

So if you are also using arc42 (or want to), consider this an open invitation to compare notes.
Because the real magic of architecture documentation is not in the template. It is in the conversations it forces you to have.

Next up: Chapter 1, “Introduction and goals”, where we turn vague intentions into a small set of concrete quality goals.

/ Michaël Hompus

Generating PDFs in .NET is often painful—low-level drawing APIs, rigid libraries, and pricey licenses all get in the way. Playwright flips the script: design with plain HTML and CSS, then export directly to PDF. This walkthrough shows how to load a template, replace placeholders at runtime, and generate a styled PDF with Playwright in .NET.

Generating a nicely formatted PDF is surprisingly hard with traditional .NET libraries. Pixel‑perfect layout often requires low‑level drawing APIs or proprietary tooling.

When looking for a solution, I stumbled upon Microsoft Playwright, a library for end-to-end testing in the browser.

This in itself might not sound like a good fit for PDF generation, but if you know that the print preview inside a Chromium browser is actually a rendered PDF, it makes more sense.

So if we can render HTML in a browser, we can also export it to PDF. And this gives us all the flexibility of HTML and CSS for layout, without the hassle of low-level drawing APIs.

For this article, we will create a simple .NET console application that generates a PDF from an HTML template using Playwright. Playwright integration is not limited to .NET, there are also libraries for JavaScript, Python, and Java, so you can use it in your preferred language.

1. Add Playwright to the application

Start by adding the Microsoft.Playwright package. After restoring packages run the Playwright CLI to download the required browsers.

The smallest installation is Chromium with only the shell dependencies, which is sufficient for PDF generation:

Terminal window
.\bin\Debug\net9.0\playwright.ps1 install chromium --with-deps --only-shell

With the dependencies in place you can launch Chromium in headless mode:

using Microsoft.Playwright;
// Create an instance of Playwright
using var playwright = await Playwright.CreateAsync();
// Launch a Chromium browser instance
await using var browser = await playwright.Chromium.LaunchAsync();

2. Create an HTML template with placeholders

Create a template that contains placeholders for the dynamic parts. The demo project ships with html-template.html embedded as a resource.

It also shows how the CSS page rules can be used to set the page size, margins, and headers/footers:

html-template.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>{{title}}</title>
<style>
@page {
size: A4;
margin: 2cm;
@top-center { content: "Page Header"; }
@bottom-right { content: counter(page) " of " counter(pages); }
}
</style>
</head>
<body>
<h1>PDF Generation Demo</h1>
<p>{{body}}</p>
<p><address>https://blog.hompus.nl</address></p>
</body>
</html>

3. Replace placeholders at runtime

Load the template and swap the placeholders for real values before rendering. To keep things simple, we’ll generate some random body text with a Lorem Ipsum library:

// Create a temporary directory to store the generated files
var tempDir = Directory.CreateTempSubdirectory("pdfs_");
// Define the path for the HTML file that will be used as input for the PDF
var outputPath = Path.Combine(tempDir.FullName, "pdf-input.html");
// Load the embedded HTML template resource from the assembly
using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("CreatePdfs.Playwright.html-template.html");
// Read the HTML template into a byte buffer
Span<byte> buffer = new byte[stream.Length];
stream.ReadExactly(buffer);
// Convert the byte buffer to a UTF-8 string
var templateHtml = Encoding.UTF8.GetString(buffer);
// Generate placeholder content for the body using Lorem.NETCore
// Settings: Generate 20 paragraphs with between 3 and 8 sentences per paragraph, and between 8 and 10 words per sentence.
var generatedBody = string.Join("</p><p>", LoremNETCore.Generate.Paragraphs(8, 20, 3, 8, 20));
// Replace placeholders in the HTML template with actual content
using var outputFile = File.CreateText(outputPath);
outputFile.Write(templateHtml!
.Replace("{{title}}", "Hello, World!") // Replace the title placeholder
.Replace("{{body}}", generatedBody) // Replace the body placeholder with generated content
);
outputFile.Close();

4. Generate the PDF

Finally instruct Playwright to navigate to the generated HTML file and export a PDF:

// The output path for the generated PDF file
var pdfPath = outputPath.Replace(".html", ".pdf");
// Create a new browser page
var page = await browser.NewPageAsync();
// Navigate to the generated HTML file
await page.GotoAsync(outputPath); // The browser can load files from the local file system
// Generate a PDF from the HTML content
await page.PdfAsync(new PagePdfOptions
{
DisplayHeaderFooter = true, // Enable header and footer in the PDF
Landscape = false, // Use portrait orientation
PreferCSSPageSize = true, // Use CSS-defined page size
Tagged = true, // Enable tagged PDF for accessibility (e.g., helps screen readers navigate)
Path = pdfPath, // Define the output path for the PDF
Outline = true, // Include an outline (bookmarks) in the PDF
});

The resulting file can be saved, returned from an API or just opened. The complete example project is available on GitHub.

Notes and next steps

  • Background jobs – The same code runs perfectly in a background worker like Hangfire, letting you offload PDF generation from the request pipeline.

  • Fonts and styling – Any font that the browser can render can be embedded via CSS. Add @font-face rules and Playwright will include the fonts in the PDF. Just remember that fonts might not be available in a container, so to be safe, add the font as woff files to your project and reference them in the CSS.

    @font-face {
    font-family: 'Open Sans';
    src: url('/fonts/OpenSans.woff') format('woff');
    }
    body { font-family: 'Open Sans', sans-serif; }

Playwright may not be the first tool you think of for PDF generation, but it gives you a full browser engine and a flexible API.

It turns out the best PDF library is… a browser pretending it’s a printer.

Filed under C#
Last update: