Design

The design turns the Event Storming board into the model the services implement, at the two levels DDD distinguishes. The strategic design works from the problem space: it finds the subdomains of the business, ranks them, and settles the bounded contexts and their relations. The tactical design works in the solution space: for each bounded context it describes the aggregates, the rules they protect and the events they publish.

Every name below is a term of the ubiquitous language, and every aggregate is one of the large yellow stickies of step 9.

Strategic Design

Subdomains

Subdomains are discovered, bounded contexts are designed. The subdomains exist in the business before any design, so they were read off the timeline of the board: each pivotal event opens a phase of the business, and the events that follow it, up to the next pivotal event, belong to the same area.

Subdomain Opened by Events Type
Identity User Registered User Registered, User Signed In Generic
Workspace organisation Workspace Created Workspace Created, Workspace Renamed, Workspace Deleted, Main Workspace Changed Supporting
Membership and roles Member Invited Member Invited, Member Joined, Invitation Declined, Member Role Changed, Member Removed, Member Left Supporting
Page content Page Created, Block Inserted Page Created, Page Metadata Changed, Page Moved, Page Deleted, Block Inserted, Block Updated, Block Moved, Block Deleted, Block Tree Deleted Core
Real-time collaboration Session Joined Session Joined, Edits Merged, Session Left, Session Closed Core
Discussion Comment Posted Comment Posted, Thread Resolved Supporting
Notification Notification Raised Notification Raised, Notification Delivered, Notification Read Generic

A core subdomain is complex and is what sets the product apart, a supporting one is specific to the product but simple and gives no advantage, a generic one is complex or not but solved the same way by everyone.

Bounded Contexts

The bounded contexts are the ones drawn at step 10 of the Event Storming. Two of them hold two subdomains each.

Bounded context Subdomains Aggregates
Account Identity, Workspace organisation User, Workspace
Membership Membership and roles Workspace Membership
Editing Page content, Real-time collaboration Page, Page Block Tree, Real-time Editing Session
Discussion Discussion Comment Thread
Notification Notification Notification

Editing keeps page content and real-time collaboration together because an edit merged by the session turns at once into commands on the Page Block Tree; splitting them would put a network hop inside the path that QA-02 bounds at 300 ms. Account keeps identity and workspace organisation together because a user’s first workspace is created when the user registers, and the main workspace belongs to the user. Account therefore mixes a generic subdomain with a supporting one: if identity is covered by an existing component, Account is where it plugs in.

Context Map

The policies of the board that cross the boundaries of the bounded contexts are their integration points, and those of the future microservices:

From Event Policy To
Account Workspace Created Accept the creator as Admin Membership
Account Workspace Created Create its root page Editing
Account Workspace Deleted Delete its root page Editing
Membership Member Invited Send the invitation Notification
Discussion Comment Posted Notify the mentioned user Notification
Account Workspace Deleted Delete its membership Membership
Editing Page Deleted Delete its comment threads Discussion
Editing Block Deleted Delete its comment threads Discussion

Besides these policies, Account, Editing and Discussion need the roles held by Membership, as listed under Roles.

Tactical Design

Building blocks

Each bounded context has its own model, drawn as a UML class diagram with the DDD stereotypes: Aggregate Root, Entity, Value Object, Repository. A box groups the classes of one aggregate. Every aggregate root has a repository that stores and loads it whole; factories and domain services are named in the text where creation or a rule needs one.

Three rules shape every aggregate:

  1. Only the root is referenced from outside. Inner entities, such as the blocks of a Page Block Tree, change only through a command on the root, which checks the invariants first.
  2. Aggregates refer to each other by identity. A Page holds a WorkspaceId, never the Workspace. Across contexts the identities are the only thing shared: UserId, WorkspaceId, PageId, BlockId.
  3. One command changes one aggregate, in one transaction. A rule that spans two aggregates is kept by a policy that reacts to an event, so it holds eventually rather than at once. Those rules are collected in Rules across aggregates.

For each aggregate, the commands are listed with who issues them: an actor, named by the role the command requires, or a policy. A role is held by Membership, so the commands of the other contexts check it from outside, as summarised in Roles.

Account

classDiagram
  direction LR
  namespace User_aggregate {
    class User {
      <<Aggregate Root>>
      id: UserId
      email: Email
      password: PasswordHash
      mainWorkspace: WorkspaceId
    }
    class Email {
      <<Value Object>>
    }
    class PasswordHash {
      <<Value Object>>
    }
  }
  namespace Workspace_aggregate {
    class Workspace {
      <<Aggregate Root>>
      id: WorkspaceId
      name: WorkspaceName
    }
    class WorkspaceName {
      <<Value Object>>
    }
  }
  class UserRepository {
    <<Repository>>
  }
  class WorkspaceRepository {
    <<Repository>>
  }
  User --> Email
  User --> PasswordHash
  Workspace --> WorkspaceName
  User ..> Workspace : mainWorkspace, by id
  UserRepository ..> User : stores
  WorkspaceRepository ..> Workspace : stores

User

The identity that signs in (US-01), and the owner of the user’s preferences.

Command Issued by Event
Register Visitor User Registered
Sign in User User Signed In
Set main workspace User; policy whenever a user’s first workspace is created, make it their main workspace Main Workspace Changed

The main workspace is a preference of one user, while a workspace is shared by all its members. Keeping it on Workspace, as the board did, would need a flag per member, and changing the main workspace would update two workspaces in one transaction. The active workspace is not in the model at all: switching workspace is a navigation choice of the client, with no rule to protect and no policy reacting to it, served by the Workspace directory read model.

Workspace

A separate space of pages (US-02). The name is a WorkspaceName, never blank.

Command Issued by Event
Create workspace User; policy whenever a user registers, create their first workspace Workspace Created
Rename workspace Admin Workspace Renamed
Delete workspace Admin Workspace Deleted

Membership

classDiagram
  direction LR
  namespace Workspace_Membership_aggregate {
    class WorkspaceMembership {
      <<Aggregate Root>>
      workspace: WorkspaceId
      members: Member[1..*]
      invitations: Invitation[*]
    }
    class Member {
      <<Entity>>
      user: UserId
      role: Role
    }
    class Invitation {
      <<Entity>>
      id: InvitationId
      invitee: UserId
      role: Role
      invitedBy: UserId
    }
    class Role {
      <<Value Object>>
      Admin
      Editor
      Viewer
    }
  }
  class WorkspaceMembershipRepository {
    <<Repository>>
  }
  WorkspaceMembership "1" *-- "1..*" Member
  WorkspaceMembership "1" *-- "*" Invitation
  Member --> Role
  Invitation --> Role
  WorkspaceMembershipRepository ..> WorkspaceMembership : stores

Workspace Membership

Who belongs to one workspace, with which role, and who has been invited to it (US-03). It is identified by the WorkspaceId of its workspace.

Command Issued by Event
Invite member Admin Member Invited
Accept invitation Invitee; policy whenever a workspace is created, accept the creator as Admin, carried out by the factory Member Joined
Decline invitation Invitee Invitation Declined
Change member role Admin Member Role Changed
Remove member Admin Member Removed
Leave workspace Member Member Left
Delete workspace membership Policy whenever a workspace is deleted, delete its membership Workspace Membership Deleted

Editing

classDiagram
  direction TB
  namespace Page_aggregate {
    class Page {
      <<Aggregate Root>>
      id: PageId
      workspace: WorkspaceId
      parent: PageId [0..1]
      metadata: PageMetadata
    }
    class PageMetadata {
      <<Value Object>>
      title
      icon
      cover
    }
  }
  namespace Page_Block_Tree_aggregate {
    class PageBlockTree {
      <<Aggregate Root>>
      page: PageId
      blocks: Block[*]
    }
    class Block {
      <<Entity>>
      id: BlockId
      type: BlockType
      content: Markdown
      children: Block[*]
      subPage: PageId [0..1]
    }
    class BlockType {
      <<Value Object>>
      Text
      Heading
      BulletedList
      NumberedList
      ToDo
      Code
      Page
    }
    class Markdown {
      <<Value Object>>
    }
  }
  namespace Real_time_Editing_Session_aggregate {
    class RealTimeEditingSession {
      <<Aggregate Root>>
      page: PageId
      presence: Presence
      closed: Boolean
    }
    class Presence {
      <<Value Object>>
      collaborators: UserId[*]
    }
    class Edit {
      <<Value Object>>
      author: UserId
      change
    }
  }
  class PageRepository {
    <<Repository>>
  }
  class PageBlockTreeRepository {
    <<Repository>>
  }
  class RealTimeEditingSessionRepository {
    <<Repository>>
  }
  Page --> PageMetadata
  PageBlockTree "1" *-- "*" Block
  Block "1" *-- "*" Block : children
  Block --> BlockType
  Block --> Markdown
  RealTimeEditingSession --> Presence
  RealTimeEditingSession ..> Edit : merges
  Page ..> Page : parent, by id
  PageBlockTree ..> Page : page, by id
  Block ..> Page : subPage, by id
  RealTimeEditingSession ..> Page : page, by id
  PageRepository ..> Page : stores
  PageBlockTreeRepository ..> PageBlockTree : stores
  RealTimeEditingSessionRepository ..> RealTimeEditingSession : stores

Page

A document of a workspace, with its metadata and its place in the page tree (US-04, US-05).

Command Issued by Event
Create page Policies whenever a workspace is created, create its root page and whenever a page block is inserted, create the sub-page Page Created
Set title, icon, cover Editor; policy whenever a page block is updated, update its metadata Page Metadata Changed
Move page Policy whenever a page block is moved, move it in the tree as well Page Moved
Delete page Policies whenever a page block is deleted, delete the page as well, whenever a page is deleted, delete its content and sub-pages and whenever a workspace is deleted, delete its root page Page Deleted

Page Block Tree

The content of one page (US-06, US-07). It is identified by the PageId of its page and starts empty together with it.

Command Issued by Event
Insert block Policy when edits are merged, apply them to the page block tree Block Inserted
Update block Same policy Block Updated
Move or nest block Same policy Block Moved
Delete block Same policy Block Deleted
Delete block tree Policy whenever a page is deleted, delete its content and sub-pages Block Tree Deleted

Real-time Editing Session

The shared editing of one page (US-08). It is identified by the PageId of its page. As agreed while reviewing the board, the session exists even with a single collaborator: the first Join session opens it, so editing alone and editing together follow the same path.

Command Issued by Event
Join session Collaborator Session Joined
Leave session Collaborator Session Left
Submit edit Editor Edits Merged
Close session Policy when a page is deleted, close its editing session and notify its participants Session Closed

Discussion

classDiagram
  direction LR
  namespace Comment_Thread_aggregate {
    class CommentThread {
      <<Aggregate Root>>
      id: ThreadId
      workspace: WorkspaceId
      anchor: Anchor
      comments: Comment[1..*]
      resolved: Boolean
    }
    class Anchor {
      <<Value Object>>
      page: PageId
      block: BlockId [0..1]
    }
    class Comment {
      <<Entity>>
      id: CommentId
      author: UserId
      text: Markdown
      mentions: Mention[*]
    }
    class Mention {
      <<Value Object>>
      user: UserId
    }
  }
  class CommentThreadRepository {
    <<Repository>>
  }
  CommentThread --> Anchor
  CommentThread "1" *-- "1..*" Comment
  Comment --> Mention
  CommentThreadRepository ..> CommentThread : stores

Comment Thread

A conversation attached to a page or to one of its blocks (US-09).

Command Issued by Event
Post comment Collaborator with the Editor role Comment Posted
Resolve thread Collaborator with the Editor role Thread Resolved
Delete thread Policies whenever a page is deleted, delete its comment threads and whenever a block is deleted, delete its comment threads Thread Deleted

Notification

classDiagram
  direction LR
  namespace Notification_aggregate {
    class Notification {
      <<Aggregate Root>>
      id: NotificationId
      recipient: UserId
      subject: Subject
      status: Status
    }
    class Subject {
      <<Value Object>>
      Invitation
      Mention
    }
    class Status {
      <<Value Object>>
      Raised
      Delivered
      Read
    }
  }
  class NotificationRepository {
    <<Repository>>
  }
  Notification --> Subject
  Notification --> Status
  NotificationRepository ..> Notification : stores

Notification

A message telling one user about an invitation or a mention (US-10).

Command Issued by Event
Raise notification Policies when MemberInvited then send the invitation and when another user is mentioned Notification Raised
Deliver notification Policy notification delivery with best effort Notification Delivered
Mark as read User Notification Read

Rules across aggregates

The rules that span more than one aggregate, and the policies that keep them. Between the event and the reaction the rule does not hold yet: right after Workspace Created, for instance, the root page may not exist, and the read models must allow for it.

Rule Kept by Traces to
A new user has a workspace, and it is their main one Whenever a user registers, create their first workspace; whenever a user’s first workspace is created, make it their main workspace US-01, US-02
A workspace has an Admin from its creation Whenever a workspace is created, accept the creator as Admin US-03
A workspace has exactly one root page Whenever a workspace is created, create its root page, plus the check of the Page factory US-04
Every page block has its sub-page, and every sub-page its page block Whenever a page block is inserted, moved, deleted, updated… US-04
Merged edits reach the content of the page When edits are merged, apply them to the page block tree US-08
Deleting a workspace deletes everything in it Whenever a workspace is deleted, delete its root page, then the cascade below; whenever a workspace is deleted, delete its membership US-02, PP-05
Deleting a page deletes its content, sub-pages, session and threads Whenever a page is deleted, delete its content and sub-pages; when a page is deleted, close its editing session; whenever a page is deleted, delete its comment threads US-04, US-09, PP-05
Deleting a block deletes the threads anchored to it Whenever a block is deleted, delete its comment threads US-09, PP-05
Invited and mentioned users are told When MemberInvited then send the invitation; when another user is mentioned US-10

Roles

The roles live in Membership, but most of the commands that need them belong to other contexts. The table states which role each command requires; how Account, Editing and Discussion learn the roles is PP-04, settled with the context map.

Command Context Admin Editor Viewer
Rename workspace, Delete workspace Account ✓    
Invite member, Change member role, Remove member Membership ✓    
Leave workspace Membership ✓ ✓ ✓
Set title, icon, cover; Submit edit Editing ✓ ✓  
Join session Editing ✓ ✓ ✓
Post comment, Resolve thread Discussion ✓ ✓  

Two more checks read Membership without a role: the main workspace of a user is one the user is a member of, and a mention names a member of the workspace.

Changes from the board

The model refines the board, and these changes are to be carried back to it:

Architecture

Components and Connectors

Hexagonal Architecture

Microservices

Patterns