Tutorial: A Paint App

Step 2: The Domains

Split the problem into three areas, and say where the boundaries are.

Three areas of the problem, each one something you could assign work to: the bitmap, the tools that draw into it, and getting drawings on and off disk.

Canvas

src/domains/Canvas.psy:

use "@psy/morphic"

export domain Canvas:
    description: The bitmap, and everything that writes to it.

    covers:
        - The pixel buffer and its fixed dimensions
        - Compositing a stamp onto the buffer
        - Undo snapshots and restoration
        - Presenting the buffer on screen at the current zoom

    excludes:
        - Deciding what to stamp. That is Tools. The canvas is told what shape
          and colour to write, never which tool asked.

    invariants:
        - The buffer is never resized after a drawing is created
        - Nothing outside this domain writes a pixel

Tools

src/domains/Tools.psy:

use "@psy/morphic"

export domain Tools:
    description: What the user draws with, and how one is chosen.

    covers:
        - The set of available tools and which is active
        - Tool options: size, colour, hardness
        - Turning pointer input into stamps

    excludes:
        - Writing pixels. Tools produce stamps; the canvas applies them.
        - The colour values themselves. Those belong to ColorPicker, which
          Tools reads from.

    invariants:
        - Exactly one tool is active at all times. There is no null tool.
        - Switching tools mid-stroke is impossible; the stroke ends first.

Files

src/domains/Files.psy:

use "@psy/morphic"

export domain Files:
    description: Creating, saving and opening drawings.

    covers:
        - Creating a new drawing at a chosen size
        - Encoding the buffer as PNG and writing it
        - Decoding a PNG into a buffer
        - Tracking whether there are unsaved changes

    excludes:
        - Prompting the user. The dialogs belong to the UI; this domain
          answers "is it safe to discard" and does the reading and writing.

    invariants:
        - Saving never modifies the buffer
        - A failed write leaves the previous file intact

Check it:

psy check
check: no problems found in 6 file(s)

Why excludes is on all three

Every one of those exclusions is a boundary between two domains that obviously touch — and each names where the responsibility actually lands.

Read the Canvas and Tools exclusions together:

Canvas.excludes:  Deciding what to stamp. That is Tools.
Tools.excludes:   Writing pixels. Tools produce stamps; the canvas applies them.

That is one decision, stated from both sides, and it is the decision that makes StrokeModel implementable. Somebody adding a tool now cannot reasonably put a putPixel call in it — the boundary is written down in the two places they would look.

Boundaries stated only from the inside are half a boundary. The near-misses are where mistakes actually happen, because they are the plausible-looking wrong answers.

What they generate

.claude/skills/tools/SKILL.md:

---
name: tools
description: "Domain: What the user draws with, and how one is chosen."
---

# Tools

## Covers

- The set of available tools and which is active
- Tool options: size, colour, hardness
- Turning pointer input into stamps

## Excludes

- Writing pixels. Tools produce stamps; the canvas applies them.
- The colour values themselves. Those belong to ColorPicker, which Tools reads
  from.

## Invariants

- Exactly one tool is active at all times. There is no null tool.
- Switching tools mid-stroke is impossible; the stroke ends first.

## Authority

This describes the product as it is meant to work. Where the implementation
disagrees, treat this as correct unless the task is explicitly to change it.

Three, not eight

We could have had a Colour domain, a History domain, a UI domain, an Input domain. We do not, because none of them is an area you would assign a week of work to in a program this size — they are concerns inside the three we have.

Colour lives in Tools' covers. History lives in Canvas'. Input is split across Tools and StrokeModel. When one of them grows enough that somebody could plausibly own it, promote it then.

Three to seven is the useful range. Eight domains in a program with five features means the domains are really components.

Next: The Features

On this page