Make it concrete: one command, one port
Anchor the architecture in a single SaaS action: inviting a teammate to a workspace.
Abstractions evaporate without a concrete anchor, so here’s the smallest real thing. Take a single action in a SaaS product: inviting a teammate to a workspace.
The request arrives at a primary adapter (an HTTP endpoint). It hands the core a single instruction — invite this person to this workspace — and steps back. Inside the core, one piece of code is responsible for that instruction. Call it a command handler. It loads the workspace through a repository port, applies the rules (is there a free seat on the plan? is this person already a member?), and saves the result back through that same port.
// illustrative — not stack-specific
InviteTeammateHandler.handle(command):
workspace = repository.load(command.workspaceId) // secondary port
workspace.invite(command.email) // business rule lives here
repository.save(workspace) // secondary port
Three things to notice, because they carry the rest of the course:
- The handler is the single entry point for this behaviour. There’s one obvious place the rule lives, and one obvious place to test it.
- The handler talks to the database only through the repository port. It has no idea what’s behind it.
- The actual decision — can this invite happen? — lives in the core, in language a product person would recognise. Not in the controller, not in the database.