ArchitectureSystem DesignVisual Design

Visual System Design: Using an Architecture Canvas to Map Your Product

How to think about entities, APIs, and data models visually before writing a single line of code.

Intent Team9 min read

Most software starts as an idea described in words. A product manager writes a document. An engineer reads it, forms a mental model, and starts coding. The problem is that the mental model in the engineer's head rarely matches the one in the PM's head, and neither matches the one in the other engineer's head.

Words are imprecise for describing systems. A sentence like "users can create projects and invite team members" hides an enormous amount of structural complexity. What's the relationship between users and projects? Is it many-to-many? Is there a membership model? What does "invite" mean, an email flow, a direct add, or both? What permissions does a team member have?

Visual system design forces you to answer these questions before you write code. When you put entities on a canvas and draw lines between them, gaps in your thinking become obvious. Missing relationships, ambiguous ownership, undefined flows -- they're all invisible in a text document and immediately visible in a diagram.

This article covers how to use an architecture canvas to map your product's structure: data models, API endpoints, and logic flows. It's not about UML or formal diagramming methods. It's about practical visual thinking that translates directly into working software.

The Three Pillars of Product Architecture

Every product, regardless of complexity, is built on three structural pillars. Understanding and defining these before coding starts is what separates a planned architecture from an accidental one.

Data Models

Data models are the nouns of your system. They represent the things your application stores and operates on: users, projects, tasks, comments, files, labels. Each model has fields with types, constraints, and relationships to other models.

On a canvas, a data model looks like a card with the model name and its key fields:

┌─────────────────────┐
│       Project       │
├─────────────────────┤
│ id: uuid            │
│ name: string        │
│ description: text   │
│ owner_id: uuid → User│
│ created_at: timestamp│
└─────────────────────┘

The value of putting this on a canvas rather than a spreadsheet is that you can see relationships. When Project has an owner_id pointing to User, you draw a line between them. When Task belongs to Project and is assigned to User, those lines make the dependency structure visible at a glance.

API Endpoints

API endpoints are the verbs. They define what operations the system supports: creating a project, listing tasks, updating a user's profile, deleting a comment. Each endpoint has a method, a path, input parameters, and output shapes.

On a canvas, endpoints are typically grouped by the resource they operate on and placed near the data model they affect:

┌───────────────────────────┐
│     Project Endpoints     │
├───────────────────────────┤
│ POST   /projects          │
│ GET    /projects           │
│ GET    /projects/:id       │
│ PUT    /projects/:id       │
│ DELETE /projects/:id       │
│ POST   /projects/:id/members│
└───────────────────────────┘

Placing endpoints visually near their related models makes it obvious when something is missing. If you have a ProjectMember model but no endpoint to create one, the gap is visible. If you have a delete endpoint but haven't thought about what happens to related tasks, the canvas makes you confront that question.

Logic Flows

Logic flows are the sequences. They describe what happens when a user does something: the chain of operations from user action through API to database and back. Authentication flows, invitation workflows, payment processing, notification triggers.

On a canvas, a logic flow is a connected series of steps:

User clicks "Invite" →
  Frontend validates email →
    POST /projects/:id/invitations →
      Check if user exists →
        Yes: Create ProjectMember, send notification
        No: Create Invitation record, send invite email →
          User signs up → Invitation auto-accepts → Create ProjectMember

Logic flows are where the most important architectural decisions live. They reveal ordering dependencies, error handling requirements, and the interactions between different parts of the system that aren't obvious from looking at models and endpoints in isolation.

Mapping Entities and Their Relationships

The most valuable exercise you can do on an architecture canvas is mapping your entities and their relationships. Here's a practical approach.

Start with the core entities

Every product has two to four core entities that everything else revolves around. For a project management tool, it might be User, Organization, Project, and Task. For an e-commerce platform: User, Product, Order, Payment.

Place these on the canvas first, spaced apart to leave room for related entities.

Add relationships

For each pair of core entities, ask: what's the relationship?

  • One-to-many: A User creates many Projects. Draw a line from User to Project labeled "creates / owns."
  • Many-to-many: A User can be a member of many Projects, and a Project can have many Users. This means you need a join entity. Add ProjectMember to the canvas.
  • One-to-one: Rare, but sometimes a User has one Profile. Usually this is a signal that the two models should be merged.
┌──────┐     owns      ┌─────────┐
│ User │───────────────→│ Project │
└──────┘                └─────────┘
    │                       │
    │   ┌──────────────┐    │
    └──→│ProjectMember │←───┘
        │  role: enum  │
        │  joined_at   │
        └──────────────┘

Add secondary entities

Once core relationships are mapped, add the entities that depend on them. Tasks belong to Projects. Comments belong to Tasks. Labels belong to Projects and can be applied to Tasks. Attachments belong to Comments.

Each new entity either extends a core entity or creates a new relationship between existing ones. The canvas should grow outward from the core.

Look for missing entities

This is where the canvas earns its keep. When you step back and look at the full picture, you'll notice gaps. Common ones:

  • Join tables that need their own data. If a User can be a member of a Project with a specific role, you need a ProjectMember model with a role field. A simple many-to-many join won't cut it.
  • Event/history entities. If your system needs to track who did what and when, you might need an ActivityLog or AuditEvent model.
  • Configuration entities. If projects have settings, those might belong in a ProjectSettings model rather than cramming everything into the Project model.

Designing APIs from the Architecture View

Once your data models and relationships are on the canvas, designing APIs becomes more systematic.

CRUD as a starting point

For each entity on the canvas, start with the standard CRUD operations:

// For the Task entity:
POST   /api/v1/projects/:projectId/tasks       // Create
GET    /api/v1/projects/:projectId/tasks       // List
GET    /api/v1/projects/:projectId/tasks/:id   // Get
PUT    /api/v1/projects/:projectId/tasks/:id   // Update
DELETE /api/v1/projects/:projectId/tasks/:id   // Delete

Not every entity needs all five. Some entities are create-only (like audit logs). Some are read-only from the user's perspective (like computed analytics). The canvas helps you decide because you can see how the entity is used in logic flows.

Relationship operations

Relationships often need their own endpoints. If a Task can have Labels, you need:

POST   /api/v1/tasks/:taskId/labels        // Apply label
DELETE /api/v1/tasks/:taskId/labels/:labelId  // Remove label

On the canvas, these endpoints sit on the line between the two entities, making it clear they represent the relationship, not either entity alone.

Action endpoints

Some operations don't fit CRUD. "Archive a project" isn't really an update; it's a specific action with side effects (archiving all tasks, notifying members, etc.). These are better modeled as explicit action endpoints:

POST /api/v1/projects/:projectId/archive
POST /api/v1/projects/:projectId/restore
POST /api/v1/tasks/:taskId/assign

On the canvas, action endpoints connect to logic flows. The "archive" endpoint triggers a flow that cascades through related entities.

Watch for N+1 patterns

A common API design mistake is requiring the client to make too many requests to assemble a view. If a page shows a project with its tasks, members, and recent activity, you don't want the client making four separate API calls.

The canvas helps you spot this. Look at any UI screen and trace which entities it needs. If it touches three or more entities, consider an aggregated endpoint or making sure your list endpoints support including related data:

GET /api/v1/projects/:id?include=tasks,members,activity

When to Use Visual vs. Text-Based Design

Visual and text-based design serve different purposes. Neither replaces the other.

Use visual design when:

  • You're starting a new feature and need to understand entity relationships
  • Multiple people need to agree on system structure
  • You're looking for gaps, missing entities, or undefined relationships
  • The feature involves complex flows with branching logic
  • You want to see how a change ripples through the system

Use text-based specs when:

  • You need to define exact field types, validation rules, and constraints
  • You're documenting API contracts with request/response payloads
  • You're writing acceptance criteria that need to be testable
  • The feature is well-understood and you're specifying details, not exploring structure

The best workflow uses both. Start on the canvas to map the structure. Then write the detailed spec using what the canvas revealed. The visual design answers "what are the pieces and how do they connect?" The text spec answers "what exactly does each piece look like?"

Practical Example: Designing a Notifications Feature

Let's walk through designing a notifications feature from an architecture-first perspective.

Step 1: Identify entities

Starting question: what does a notification need to represent?

Notification
├── id: uuid
├── recipient_id: uuid → User
├── type: enum (mention, assignment, comment, status_change)
├── title: string
├── body: text
├── read: boolean (default: false)
├── resource_type: string (task, project, comment)
├── resource_id: uuid
├── created_at: timestamp

On the canvas, Notification connects to User (the recipient) and has a polymorphic reference to the resource it's about (a task, project, or comment).

Step 2: Map the trigger flows

Notifications don't appear from nowhere. Something creates them. Map the triggers:

Task assigned → Create notification for assignee (type: assignment)
Comment added → Create notification for task watchers (type: comment)
User @mentioned → Create notification for mentioned user (type: mention)
Task status changed → Create notification for task owner (type: status_change)

Each trigger is a logic flow. On the canvas, draw lines from the source action to the Notification entity. This immediately reveals a design question: should notifications be created synchronously in the endpoint handler, or asynchronously via a background job?

Step 3: Design the API

From the canvas, the notification API takes shape:

GET    /api/v1/notifications              // List my notifications
GET    /api/v1/notifications/unread-count // Get unread count
PUT    /api/v1/notifications/:id/read     // Mark as read
POST   /api/v1/notifications/mark-all-read // Mark all as read

No create endpoint, because notifications are created by the system, not by users. No delete endpoint, because the product decision is that notifications are permanent. These decisions are visible on the canvas because the creation lines come from other entities, not from a user action.

Step 4: Spot what's missing

Looking at the canvas, a few things jump out:

  • Notification preferences. Users need to control which notifications they receive. Add a NotificationPreference entity connected to User.
  • Batching. If someone is mentioned ten times in rapid succession, they shouldn't get ten separate notifications. This is a logic flow concern: add a debounce step to the trigger flows.
  • Resource deletion. What happens to a notification when the task it references is deleted? The polymorphic reference means you can't use a foreign key cascade. You need to handle this in application logic, or accept that some notifications will reference deleted resources and handle that in the UI.

None of these are obvious from a text description. All of them are visible on a canvas.

Getting Started

You don't need a specialized tool to do visual system design. A whiteboard works. A drawing app works. The point isn't the tool; it's the practice of putting entities in space and drawing relationships between them.

That said, a canvas built specifically for product architecture, one that understands data models, endpoints, and flows as first-class concepts, reduces friction. You spend less time drawing boxes and more time thinking about structure.

Start with your next feature. Before writing any code, spend thirty minutes placing the entities on a canvas, drawing the relationships, and mapping the key flows. You'll find gaps you would have discovered three days into implementation. Finding them on a canvas costs minutes. Finding them in code costs hours.

Related articles

Ready to write specs that actually work?

Intent helps you structure product ideas, generate visual previews, and export specs your dev team and AI tools can use immediately.

Start free trial

7-day trial · Full access · Cancel anytime