Layered architecture
A layered diagram stacks the tiers of a system, one wide layer for each, with the blocks that live in each layer and the few links that show how the layers call each other. It is the picture that opens almost every design document: presentation, application, data, infrastructure. It is for anyone, human or AI agent, who writes diagram source.
TL;DR
| Question | Answer |
|---|---|
| What is it for? | The shape of a system by tier: which blocks sit in which layer, and which layer calls which. Use a flow diagram when the picture is about how a request moves, and layers when it is about where things live. |
| How do I start one? | Write layers "Title", then layer "Name" { blocks with block id "Label" lines inside, and } to close each layer. Write a link between two blocks as web -> api : "REST". |
| What is drawn? | Each layer is a wide frame with a corner tab and a quiet wash of its tone. Each block is a tinted card with an icon, a label, and a note. Links run in the gaps between the rows and never cross a block. |
| Which way does it stack? | The first layer is on top. direction=up puts it at the bottom instead, for a stack built on its foundation. |
| What cuts across every layer? | An aside: a tall band beside the stack, such as security or monitoring, with its own blocks. It takes no links, because it applies to every layer. |
| How wide is a block? | A layer divides its width into equal columns, and a block is one column wide. span=2 makes a block as wide as two, so a database can be wider than a service. |
| What are the limits? | 12 layers, 2 asides, 80 blocks, and 80 links. Past one, the source is refused with E140. More than 8 blocks in a layer, or more than 20 links, draws a warning. |
| Does Mermaid convert? | Not yet. Mermaid block-beta is a grid of blocks with no layers. See From Mermaid. |
Ask your agent
Use Plotwork to draw a layered diagram of our online banking platform. Four layers: Experience (web app, mobile app, API gateway), Domain services (accounts, payments, ledger), Data (PostgreSQL, Redis, Kafka) and Infrastructure (Kubernetes, the private network). Put security and monitoring in an aside beside the layers, and show the calls between the layers with their protocols.Use Plotwork to draw a three-tier web app: a browser and a CDN, a REST API with an auth service and background jobs, and a database and an object store. Show which blocks call which, and label each call.Example
layers "Online banking platform" desc="A customer reaches accounts, payments and cards through one gateway. Each layer calls the layer below it, and security and monitoring cut across all of them."
layer "Experience" tone=blue icon=lucide:monitor { block web "Web app" sub="React" icon=simple:react block mobile "Mobile app" sub="Swift and Kotlin" icon=lucide:smartphone block gateway "API gateway" sub="Routing and limits" icon=lucide:route}
layer "Domain services" tone=violet icon=lucide:boxes { block accounts "Accounts" icon=lucide:wallet block payments "Payments" icon=lucide:credit-card block ledger "Ledger" icon=lucide:book-open}
layer "Data" tone=aqua icon=lucide:database { block sql "PostgreSQL" sub="Accounts and ledger" icon=simple:postgresql span=2 block cache "Redis" sub="Sessions" icon=simple:redis block events "Kafka" sub="Payment events" icon=simple:apachekafka}
layer "Infrastructure" tone=neutral icon=lucide:server { block k8s "Kubernetes" sub="Three regions" icon=simple:kubernetes block net "Private network" sub="Subnets and routing" icon=lucide:cloud}
aside "Cross-cutting" icon=lucide:shield { block iam "Identity" icon=lucide:key-round block audit "Audit log" icon=lucide:scroll-text block watch "Monitoring" icon=lucide:activity}
gateway -> accounts : "REST"gateway -> payments : "REST"accounts -> sql : "SQL"ledger <-> sql : "SQL"payments ..> events : "payment events"sql --> k8s : "runs on"The four tones carry meaning: blue is what customers touch, violet is the business logic, aqua is where data lives, and neutral is the platform underneath. The PostgreSQL block has span=2, so it is as wide as two columns of the Data layer. The dotted line is an asynchronous call, the dashed line is a looser link, and the line with two heads is a two-way call. The aside stands beside the stack and is as tall as it, because security and monitoring touch every layer.
Three-tier web app
The oldest layered picture. counts=on shows how many blocks each layer holds, and the call from the CDN to the object store skips the application layer, so it runs down the lane on the right of the stack instead of through a block.
layers "Three-tier web app" counts=on desc="A browser talks to a web tier, the web tier calls an application tier, and only the application tier reaches the database."
layer "Presentation" tone=blue icon=lucide:monitor { block browser "Browser" sub="Single page app" icon=simple:react block cdn "CDN" sub="Static assets" icon=lucide:globe}
layer "Application" tone=violet icon=lucide:boxes { block api "REST API" sub="Node.js" icon=simple:nodedotjs block auth "Auth service" icon=lucide:key-round block jobs "Background jobs" icon=lucide:workflow}
layer "Data" tone=aqua icon=lucide:database { block db "PostgreSQL" icon=simple:postgresql block store "Object store" icon=lucide:hard-drive}
browser -> api : "HTTPS"cdn -> store : "origin pull"api -> db : "SQL"auth -> db : "SQL"jobs -> dbHexagonal ports and adapters
The domain depends on nothing, the application owns the ports, and the adapters on both sides plug into them. Three dashed links leave the Ports block, skip the Domain layer, and take a lane each.
layers "Hexagonal architecture" desc="Driving adapters call the application through ports, and the application reaches the outside world through driven adapters. The domain depends on nothing."
layer "Driving adapters" tone=blue icon=lucide:plug { block rest "REST controller" icon=lucide:globe block cli "Command line" icon=lucide:terminal block consumer "Message consumer" icon=lucide:radio}
layer "Application" tone=violet icon=lucide:cog { block orders "Place order" sub="Use case" icon=lucide:package block refunds "Refund order" sub="Use case" icon=lucide:receipt block ports "Ports" sub="Interfaces the core owns" icon=lucide:cable}
layer "Domain" tone=amber icon=lucide:book-open { block entities "Entities" icon=lucide:box block rules "Pricing rules" icon=lucide:gauge}
layer "Driven adapters" tone=aqua icon=lucide:plug { block repo "Order repository" sub="PostgreSQL" icon=simple:postgresql block mail "Email gateway" icon=lucide:mail block pay "Payment client" icon=lucide:credit-card}
rest -> orders : "calls"cli -> ordersconsumer -> refundsorders -> entitiesrefunds -> rulesports --> repo : "implemented by"ports --> mailports --> payA cloud platform on AWS
Provider icons work as they do in a flow diagram: find them with the search_icons tool or in the icon reference. A vendor's own icon is drawn unmodified on its own tile in place of the chip. The link from API Gateway to Lambda joins two blocks of one row, so it runs under the row, inside the layer.
layers "Serverless platform on AWS" desc="Requests enter at the edge, run in functions, and land in managed data stores. Operations cut across every layer."
layer "Edge" tone=orange icon=lucide:globe { block dns "Route 53" icon=aws:route-53 block cdn "CloudFront" icon=aws:cloudfront block waf "WAF" icon=aws:waf}
layer "Compute" tone=blue icon=lucide:cpu { block gw "API Gateway" icon=aws:api-gateway block fn "Lambda" icon=aws:lambda block flow "Step Functions" icon=aws:step-functions}
layer "Data" tone=aqua icon=lucide:database { block ddb "DynamoDB" icon=aws:dynamodb block s3 "S3" icon=aws:s3 block rds "RDS" icon=aws:rds}
aside "Operations" icon=lucide:activity { block cw "CloudWatch" icon=aws:cloudwatch block iam "IAM" icon=aws:iam}
cdn -> gw : "HTTPS"gw -> fnfn -> ddb : "reads and writes"fn -> s3flow -> rdsMicroservices, from the foundation up
With direction=up the first layer written is the foundation, at the bottom, so the picture reads from the stores up to the clients. Seven services do not fit one row at a readable width, so the layer wraps to two rows and the last row is centred. A link from a block in the upper row goes along the gap between the rows to the lane, so write the blocks that have links in the row nearest the layer they link to.
layers "Microservices" direction=up desc="Clients reach services through a gateway, and each service owns its store. The first layer written is the foundation, at the bottom."
layer "Stores" tone=aqua icon=lucide:database { block pgo "Orders DB" icon=simple:postgresql block pgu "Users DB" icon=simple:postgresql block mongo "Catalog" icon=simple:mongodb block redis "Cache" icon=simple:redis}
layer "Services" tone=violet icon=lucide:boxes { block orders "Orders" icon=lucide:package block users "Users" icon=lucide:users block catalog "Catalog" icon=lucide:book-open block pricing "Pricing" icon=lucide:receipt block notify "Notifications" icon=lucide:bell block search "Search" icon=lucide:search block reports "Reporting" icon=lucide:gauge}
layer "Gateway" tone=blue icon=lucide:route { block gw "API gateway" icon=lucide:route span=2 block bff "Web BFF" icon=lucide:monitor}
layer "Clients" tone=neutral icon=lucide:smartphone { block web "Web" icon=simple:react block ios "iOS" icon=lucide:smartphone block partner "Partners" icon=lucide:plug}
web -> bffios -> gw : "REST"partner -> gw : "REST"gw -> orders : "gRPC"bff -> catalogorders -> pgousers -> pgucatalog -> mongoorders <-> pricing : "price check"web --> pgo : "bypass"The language
One statement to a line. Text is always in double quotes, options are key=value, and # starts a comment. Indentation is for the reader and means nothing to the parser: nesting comes from the braces.
| Statement | Meaning | Example |
|---|---|---|
layers "Title" direction=<down|up> counts=<on|off> desc="<text>" | The header. Every option is optional, and so is the title. | layers "Online banking" direction=down |
desc "<text>" | The plain-language description, as a statement. The same as desc= on the header. | desc "Each layer calls the layer below it." |
layer "<Name>" tone=<tone> icon=<icon> { | Opens a layer. Write blocks inside it, and close it with } on its own line. | layer "Domain" tone=violet icon=lucide:boxes { |
block <id> "<Label>" sub="<note>" icon=<icon> tone=<tone> span=<1-6> | A card in the layer or aside that is open. | block pay "Payments" sub="Go" icon=lucide:credit-card span=2 |
aside "<Name>" tone=<tone> icon=<icon> { | Opens a band on the right of the stack, with its own blocks stacked in it. | aside "Security" icon=lucide:shield { |
<from> -> <to> : "<label>" | A link between two blocks of the layers. The label is optional. | web -> pay : "REST" |
} | Closes the layer or aside that is open, on a line of its own. | } |
A block's id is the word a link names. It starts with a letter, may contain letters, digits, _ and -, and is unique in the diagram. A link may be written before the blocks it joins.
layers "Shop"
layer "App" { block web "Storefront" icon=lucide:globe block api "API" sub="Checkout and catalog" icon=lucide:server}
layer "Data" { block db "Database" icon=lucide:database}
web -> api : "HTTPS"api -> db : "SQL"Layers and tone
A layer takes the next tone of a rotation (blue, violet, aqua, orange, amber, neutral) unless tone= sets one, and its blocks keep the tone. A block may choose its own, to mark one block out. Choose tones for meaning, as the example does, and nobody writes a color value: the theme owns every color in light and dark. Green and red are left out of the rotation because they read as good and bad news; choose them with tone= when they should mean that.
Blocks, columns, and spans
A layer divides its width into equal columns and puts one block in each, 12px apart. When the blocks fit one row with every word whole, they fill it, so a layer of three blocks has three wide cards. When they do not, the blocks wrap to the fewest rows, as even as the order allows, and a short last row is centred so its cards keep the width of the rows above. What a card holds, the icon and the text, is centred in it, so the room at its left and its right is the same.
span=2 makes a block as wide as two columns, and the other blocks keep their one. A span can be at most 6, and never more columns than the layer has.
Asides
An aside is a tall band beside the stack for what cuts across every layer: security, monitoring, delivery. It has a dashed edge, which says it cuts across, its own corner tab, and its blocks hang from the tab one under the other. It stands on the right of the stack, as wide as its widest block or its name needs, and as tall as the stack; if its blocks need more height than the layers, the layers grow to match. Two asides stand side by side. A block in an aside takes no links, and a link to one is an error that says why.
Links
Links leave and enter the top and bottom of a block, so a line is never squeezed into the gap between two cards. Between layers that are next to each other a link runs along the gap between them. A link between two blocks of one row runs under the row, inside the layer. A link that skips a layer runs along the gap to a lane on the right of the stack, down the lane, and back along the gap above its target. Every line stays out of every block, and a link between blocks that stand over each other is straight.
| Arrow | Line | Heads | Say it for |
|---|---|---|---|
-> | solid | one, at the second block | A call or a flow of data. |
--> | dashed | one | A reply, a read, or a link that is optional or indirect. |
<-> | solid | two | A two-way call. |
<--> | dashed | two | A two-way link that is optional or indirect. |
..> | dotted | one | An asynchronous call: an event, a webhook. |
-- | solid | none | An association with no direction. |
Keep the few links that show how the layers depend on each other. A layered picture with forty links is a flow diagram that wants to be one.
Direction and counts
direction=up puts the first layer at the bottom, for a stack built on its foundation, and everything else reads the same. counts=on shows how many blocks each layer and aside holds, in a pill in its tab.
Limits and warnings
| Limit | Value | What happens past it |
|---|---|---|
| Layers | 12 | E140 before any layout, with the advice to merge layers. |
| Asides | 2 | E140. |
| Blocks, in the layers and asides together | 80 | E140. |
| Links | 80 | E140. |
| A span | 6 | E136. |
| Blocks in a layer before the density warning | 8 | W200. |
| Links before the density warning | 20 | W200. |
The picture is 880px wide, the width a Markdown column keeps text readable at, and grows taller with the rows and with the room the links need. The layout is plain arithmetic, so 80 blocks and 80 links are laid out in a few milliseconds.
A block is as wide as its column, and its label wraps to at most three lines, so a word is kept whole by giving its layer fewer, wider columns. Text is cut with an ellipsis, and W203 says so, only when no column could hold the word. The full text is always in the text description of the picture.
Every mistake comes back as a result with a code, a line, a column, and a hint. Warnings never stop the drawing.
| Code | When it fires | Fix |
|---|---|---|
E100 | A line that does not start with a keyword or a block id, a layer or an aside with no name or no block, a block outside any layer, a layer inside another, a link from a block to itself, text after a statement, or a diagram with no layers. | Read the hint, which names the line to write. |
E101, E110, E121, E122, E123 | An unterminated string, an unknown statement, an unknown tone, an unknown icon, or an unknown direction. | Read the hint, which names the nearest choice. |
E130, E131 | A block id used twice, or a link to a block that does not exist or is in an aside. | Pick another id, or declare the block, or link a block of a layer. |
E133, E134 | A layer or an aside that is never closed, or a } with nothing to close. | Add or remove the brace. |
E135, E136 | An unknown option, or a bad value for counts= or span=. | Read the hint, which says what to write. |
E140 | More than 12 layers, 2 asides, 80 blocks, or 80 links. | Merge layers, fold blocks, or split the diagram. |
W200 | A layer with no blocks, a layer with more than 8 blocks, or more than 20 links. | Fill or remove the layer, split a crowded layer in two, or keep the links that matter. |
W202 | The picture is wider than a Markdown column and its text would shrink. | Shorten the labels. |
W203 | A block label, a tab name, or a link label was cut with an ellipsis. | Shorten it, or move detail into sub=. |
From Mermaid
Mermaid block-beta is not read yet. It draws a grid of blocks with no layers and no meaning for a layer, so no mapping from it to a layered picture is right, and the converter reports it as a type it does not draw. Two things work today. Describe the system to your agent, or paste the Mermaid and say which blocks are tiers, and ask it to draw the layers; that is a better picture than a converted grid. If the Mermaid is a flowchart with subgraph blocks for the tiers, it converts to a flow diagram with a group for each tier, which keeps the arrows between the boxes. The migration guide covers the converter in full.
Use Plotwork to draw this Mermaid block diagram as a layered architecture. Make each row a layer, name the layers from what is in them, and keep the arrows as links between blocks.See also
| Doc | Why |
|---|---|
| Writing diagrams | The flow, sequence and schema languages, and the rules every kind shares |
| Examples to copy | Finished diagrams with the prompt that gets an agent to write each one |
| Architecture patterns | Checked cloud and network patterns an agent can fill in, when the picture is how traffic flows |
| Cloud architecture | Accounts, regions and networks as nested groups, when the picture is where things run |
| Migrating from Mermaid | Convert existing Mermaid diagrams, and what is dropped |
| Language reference | Every kind, node type, option and diagnostic |
| Diagram kinds | The kinds Plotwork draws and the header of each |