Skip to content
eventline Write one

Design systems the way they happen.

Describe any system as commands, events and views. Fire one, then drag the timeline back: state is only ever a fold of what already happened.

Write your own

Try it. Sample data, running in your browser.

user -> postTweet(content, media?) in UserTimeline(userId)on [user-authenticated(userId)]  --[content.length > 280]--> [tweet-rejected]  --[default]--> [    tweet-created { userId, content, media, createdAt: clock.wall() }  ]

Pick a command. The guard on each line decides which events it emits.

Event stream, sample traffic plus yours

UserTimeline
0 tweets
Likes
0
Follows
0
now

Boxes show where.

Draw a rate limiter as services and it looks like nothing else you have drawn.

  1. Client
  2. API gateway
  3. Rate limiter
  4. Redis

Drawn as boxes

Events show what happened.

Write it as events and it looks like every other system.

  1. user-authenticated{ userId }
  2. request-logged{ userId, resourceId }
  3. rate-limit-reached{ userId, retryAfter: 60s }
  4. UserQuota(userId)A view folded from the events before it.

Written as events

Four slices, one timeline.

eventline maps one to one onto the four slice patterns of Event Modeling. Nobody drew this board, and it is not a picture: point at a box and the board traces its lineage. eventline-axi diagram eventmodel derived it from the 75 lines you are about to read.

Event Modeling board derived from twitter-pure.dsl. Five commands each have their actor above and their events beneath, grouped into three consistency boundaries: UserTimeline, UserFollows and TweetFeed. The views UserTimeline, TweetDetails and UserProfile sit right of the events that feed them. The trending-analyzer and notification-worker policies sit in the lane below.

Point at any box to trace what feeds it and what reads it.

A whole API in 75 lines.

system TwitterAPI { external: stripe (payments), sentry (monitoring), cloudfront (cdn) slo: 99.95% uptime, p99 < 100ms region: us-primary, eu-secondary, ap-southeast data-classification: public, pii}
actor User { auth: session, tier: free|pro|enterprise }actor Admin { auth: oauth, mfa: required }actor Visitor { auth: none }
boundary TweetFeed(tweetId) @partition(shardKey="tweetId")boundary UserTimeline(userId) @partition(shardKey="userId")boundary UserFollows(userId) @partition(shardKey="userId")
view UserTimeline(userId) = [ user-followed*(userId, targetUserId), tweet-created*(targetUserId), tweet-retweeted*(targetUserId)] => { tweetId, userId, content, media, createdAt, likes: count(tweet-liked*(tweetId)), retweets: count(tweet-retweeted*(tweetId))} @bergcache(key="timeline:{userId}", ttl=5m)
view TweetDetails(tweetId) = [tweet-created(tweetId)] => { content, media, createdAt, likes, retweets} @bergcache(key="tweet:{tweetId}", ttl=1h)
view UserProfile(userId) = [user-created(userId), user-updated*(userId)] => { name, handle, bio, followers, following} @bergcache(key="profile:{userId}", ttl=24h)
user -> postTweet(content, media?) in UserTimeline(userId)on [user-authenticated(userId)] --[content.length > 280]--> [tweet-rejected] --[default]--> [ tweet-created { userId, content, media, createdAt: clock.wall() } ]
user -> followUser(targetUserId) in UserFollows(userId)on [user-authenticated(userId), user-exists(targetUserId)] --[alreadyFollowing]--> [follow-rejected] --[default]--> [ user-followed { userId, targetUserId, followedAt: clock.wall() } ]
user -> likeTweet(tweetId) in TweetFeed(tweetId)on [user-authenticated(userId), tweet-exists(tweetId)] --[alreadyLiked]--> [like-rejected] --[default]--> [ tweet-liked { userId, tweetId, likedAt: clock.wall() } ]
user -> retweet(tweetId) in TweetFeed(tweetId)on [user-authenticated(userId), tweet-exists(tweetId)] --[alreadyRetweeted]--> [retweet-rejected] --[default]--> [ tweet-retweeted { userId, tweetId, retweetedAt: clock.wall() } ]
user -> deleteTweet(tweetId) in TweetFeed(tweetId)on [user-authenticated(userId), tweet-created(tweetId, userId)] --[!isOwner]--> [delete-rejected] --[default]--> [ tweet-deleted { tweetId, deletedAt: clock.wall() } ]
tweet-created { content, hashtags } --[count(tweet-created*(hashtag, window: 1h)) > 10000]--> trending-analyzer -> [ trend-detected { hashtag, volume, window: "1h", detectedAt: clock.wall() } ]
tweet-liked { userId, tweetId } -> notification-worker -> sendLikeNotification(tweetId, userId)

Every system is a series of events.

Commands decide. Events remember. Views answer.

Event stream
UserTimeline(u1)
empty
TweetDetails(t1)
not created
UserProfile(u2)
0 followers

    Scroll to read the model. Each command runs as you reach it.

    The same code the CLI runs

    Now you write one.

    Edit the model. eventline's own parser and renderer, bundled into this page, redraw the board on every keystroke. Nothing leaves your browser.

    model.dslClick a box to find its lines

    The board appears here. Scroll this section into view and it loads.

    Fit

    Loading the parser.

    Point at a box on the board to trace its lineage.

    Checks What each one checks

      Questions, answered plainly.

      Is eventline something I run in production?

      No. It is a notation for describing a system, plus a CLI that reads your model files and reports on them. The demos on this page run a small stand-in for a real system, in your browser, so you can see what a model means.

      Do I need to know Event Modeling first?

      No. If you do, the mapping is direct: state change, state view, automation, and translation slices each have a line shape in eventline. If you do not, start with a command, the events it can produce, and a view that reads them.

      How is this related to event storming?

      Event storming is a workshop for finding out what happens in a domain. Event Modeling continues from there and turns the events into a design you can build from. eventline writes that design as text, so it can be reviewed and checked. Read how the three fit together.

      What is a Dynamic Consistency Boundary?

      It scopes a decision by a key, such as userId, instead of by an aggregate. A command reads only the events inside its boundary, so two users never contend for the same lock.

      Can I use it in a system design interview?

      That is what it was written for. Introduce it as a quick event model: write the commands, events, boundaries, and views before you draw infrastructure boxes. Interviewers get a precise picture of state changes, invariants, and concurrency in a few lines.

      Do I need Kafka or event sourcing infrastructure?

      No. eventline describes behavior, not infrastructure. Views name their cache or partition key as annotations, and you decide what stores the events.

      What does the eventline-axi CLI do?

      It parses a .dsl file and lists its actors, commands, events, views, boundaries, and policies. It traces dependencies, validates a model against the checklist, and exports the dependency graph as toon, JSON, or plain text.

      Run it on a real model.

      One line downloads the example and points the CLI at it. You get the whole model back.

      curl -sO https://bytesbybrandon.github.io/eventline/twitter-pure.dsl && npx -y eventline-axi twitter-pure.dsl

      Needs Node 18 or later. Swap diagrams in as the first argument to draw the model instead, or write your own in the playground.

      What it prints, abridged
      $ npx -y eventline-axi twitter-pure.dsl
      
      counts[6]:
        actors: 3
        commands: 5
        events: 16
        views: 3
        boundaries: 3
        policies: 2
      
      system:
        name: TwitterAPI

      Event log

      Scrolling into a scene and every command you fire is appended here. The highlighted link in the top bar is a view folded from this log.