Contents

Event Modeling in HCL specs

Contents

Announcing a .em.hcl language spec for Event Modeling.

It lets people document their event models as code.

Yes, there are currently projects like:

So you might ask why HCL?

I see it this way:

For this project, the block form is useful because the source resembles the concepts being discussed.

My personal motivation was:

  • I want a schema in a language I can easily edit by hand;
    • sorry to Mermaid diagrams, but they are NOT human-friendly for event modeling
    • The JSON spec is also not something I am willing to write by hand
  • I want to store it in a git repo
  • And I want it to be rendered as a nice HTML event model that can live on GitLab/GitHub Pages as my system documentation.

Hence, I started this project. Writing Terraform files (HashiCorp created the HCL language) is pretty nice: it provides validation, rules, and syntax highlighting. So I thought:

  • (It’s not yet searching for John Connor) I could ask an AI to vibe-slop me the project based on my inputs.
  • I can use it in my own personal projects as the source of truth: Spec-Driven Development, done as a spec, not Markdown (sorry, .md is not a spec). SysML is a spec, .feature (Gherkin) files are a spec, .hcl, .json, .yaml are specs, but not .md files.

Here’s a simple example from the official eventmodeling.org site: /images/weather-system-em-hcl-example.png

# Appointment scheduling with state change, state view, automation, and translation workflows.

actor "scheduler" {
 title         = "Scheduler"
 auth_required = true
}

actor "calendar_user" {
 title         = "Calendar user"
 auth_required = true
}

system "weather_provider" {
 title    = "Weather provider"
 external = true
}

bounded_context "appointments" {
 title = "Appointments"

 aggregate "appointment" {
 }

 field_type "appointment_id" {
   type         = "UUID"
   id_attribute = true
 }

 field_type "starts_at" {
   type = "DateTime"
 }

 event "appointment_added" {
   title     = "Appointment Added"
   aggregate = aggregate.appointment

   field "appointment_id" {
     type = field_type.appointment_id
   }

   field "starts_at" {
     type = field_type.starts_at
   }
 }
}

bounded_context "weather" {
 title = "Weather"

 aggregate "weather_forecast" {
 }

 field_type "forecast" {
   type = "String"
 }

 field_type "changed_at" {
   type = "DateTime"
 }

 event "weather_predicted_for_appointment" {
   title     = "Weather predicted for appointment"
   aggregate = aggregate.weather_forecast

   field "appointment_id" {
     type = field_type.appointments.appointment_id
   }

   field "forecast" {
     type = field_type.forecast
   }
 }

 event "updated_weather_prediction" {
   title     = "Updated Weather Prediction"
   aggregate = aggregate.weather_forecast

   field "appointment_id" {
     type = field_type.appointments.appointment_id
   }

   field "forecast" {
     type = field_type.forecast
   }
 }
}

bounded_context "weather_provider" {
 title    = "Weather Provider"
 owner    = system.weather_provider
 external = true

 event "weather_forecast_changed" {
   title = "Weather Forecast Changed"

   field "appointment_id" {
     type = field_type.appointments.appointment_id
   }

   field "forecast" {
     type = field_type.weather.forecast
   }

   field "changed_at" {
     type = field_type.weather.changed_at
   }
 }
}

state_change "schedule_appointment" {
 title = "Schedule Appointment"

 screen "schedule_appointment_ui" {
   title = "UI"
   actor = actor.scheduler
   to    = [command.add_appointment]
 }

 screen "post_appointment" {
   title = "POST /appointment"
   to    = [command.add_appointment]
 }

 command "add_appointment" {
   title     = "Add Appointment"
   aggregate = aggregate.appointments.appointment
   to        = [event.appointments.appointment_added]

   field "appointment_id" {
     type = field_type.appointments.appointment_id
   }

   field "starts_at" {
     type = field_type.appointments.starts_at
   }
 }

 scenario "appointment_is_scheduled" {
   title = "Schedule an appointment"

   when {
     title   = "Add Appointment"
     command = command.add_appointment

     field "appointment_id" {
       type = field_type.appointments.appointment_id
     }

     field "starts_at" {
       type = field_type.appointments.starts_at
     }
   }

   then {
     title = "Appointment Added"
     event = event.appointments.appointment_added

     field "appointment_id" {
       type = field_type.appointments.appointment_id
     }

     field "starts_at" {
       type = field_type.appointments.starts_at
     }
   }
 }
}

state_view "view_calendar" {
 title = "View Calendar"

 readmodel "calendar" {
   title    = "Calendar"
   question = "Which appointments are on the calendar?"
   from     = [event.appointments.appointment_added]
   to       = [screen.calendar_ui, screen.get_calendar]

   field "appointment_id" {
     type = field_type.appointments.appointment_id
   }

   field "starts_at" {
     type = field_type.appointments.starts_at
   }
 }

 screen "calendar_ui" {
   title = "UI"
   actor = actor.calendar_user
 }

 screen "get_calendar" {
   title = "GET /calendar"
 }

 scenario "calendar_shows_scheduled_appointment" {
   title = "Show a scheduled appointment on the calendar"

   given {
     title = "Appointment Added"
     event = event.appointments.appointment_added

     field "appointment_id" {
       type = field_type.appointments.appointment_id
     }
   }

   then {
     title     = "Calendar"
     readmodel = readmodel.calendar

     field "appointment_id" {
       type = field_type.appointments.appointment_id
     }

     field "starts_at" {
       type = field_type.appointments.starts_at
     }
   }
 }
}

automation "add_weather_forecast" {
 title = "Add Weather Forecast"

 readmodel "appointments_without_weather_forecast" {
   title    = "Appointments without weather forecast"
   question = "Which appointments do not have a weather forecast?"
   from     = [event.appointments.appointment_added]
   to       = [processor.weather_processor]

   field "appointment_id" {
     type = field_type.appointments.appointment_id
   }

   field "starts_at" {
     type = field_type.appointments.starts_at
   }
 }

 processor "weather_processor" {
   title = "Weather Processor"
   to    = [command.add_weather_forecast_command]
 }

 command "add_weather_forecast_command" {
   title     = "Add Weather Forecast"
   aggregate = aggregate.weather.weather_forecast
   to        = [event.weather.weather_predicted_for_appointment]

   field "appointment_id" {
     type = field_type.appointments.appointment_id
   }

   field "forecast" {
     type = field_type.weather.forecast
   }
 }

 scenario "forecast_is_added_for_new_appointment" {
   title = "Add a weather forecast for a scheduled appointment"

   given {
     title = "Appointment Added"
     event = event.appointments.appointment_added

     field "appointment_id" {
       type = field_type.appointments.appointment_id
     }

     field "starts_at" {
       type = field_type.appointments.starts_at
     }
   }

   given {
     title     = "Appointments without weather forecast"
     readmodel = readmodel.appointments_without_weather_forecast
   }

   when {
     title     = "Weather Processor"
     processor = processor.weather_processor
   }

   then {
     title = "Weather predicted for appointment"
     event = event.weather.weather_predicted_for_appointment

     field "appointment_id" {
       type = field_type.appointments.appointment_id
     }

     field "forecast" {
       type = field_type.weather.forecast
     }
   }
 }
}

translation "translate_weather_change" {
 title = "Translate Changed Weather"

 readmodel "changed_predictions" {
   title    = "Changed Predictions"
   question = "Which external weather predictions changed?"
   from     = [event.weather_provider.weather_forecast_changed]
   to       = [processor.translator]

   field "appointment_id" {
     type = field_type.appointments.appointment_id
   }

   field "forecast" {
     type = field_type.weather.forecast
   }
 }

 processor "translator" {
   title = "Translator"
   to    = [command.translate_changed_weather]
 }

 command "translate_changed_weather" {
   title     = "Translate Changed Weather"
   aggregate = aggregate.weather.weather_forecast
   to        = [event.weather.updated_weather_prediction]

   field "appointment_id" {
     type = field_type.appointments.appointment_id
   }

   field "forecast" {
     type = field_type.weather.forecast
   }
 }

 scenario "external_weather_change_is_translated" {
   title = "Translate a changed external weather forecast"

   given {
     title = "Weather Forecast Changed"
     event = event.weather_provider.weather_forecast_changed

     field "appointment_id" {
       type = field_type.appointments.appointment_id
     }

     field "forecast" {
       type = field_type.weather.forecast
     }
   }

   given {
     title     = "Changed Predictions"
     readmodel = readmodel.changed_predictions
   }

   when {
     title     = "Translator"
     processor = processor.translator
   }

   then {
     title = "Updated Weather Prediction"
     event = event.weather.updated_weather_prediction

     field "appointment_id" {
       type = field_type.appointments.appointment_id
     }

     field "forecast" {
       type = field_type.weather.forecast
     }
   }
 }
}

Other examples can be found here.

TL;DR: If you’re a software engineer who uses a strongly statically typed programming language, can rely on a compiler to argue with an AI, and prefers explicit domain models (DDD) and specs (BDD), then please try Event Modeling with the .em.hcl language.

Project page is here.