Skip to content

Shows how to use Domain-Driven Design, Event Storming, Event Modeling and Event Sourcing in Heroes of Might & Magic III domain.

Notifications You must be signed in to change notification settings

MateuszNaKodach/HeroesOfDomainDrivenDesign.EventSourcing.Ruby

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

59 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Heroes of Domain-Driven Design (Ruby)

Shows how to use Domain-Driven Design, Event Storming, Event Modeling and Event Sourcing in Heroes of Might & Magic III domain.

πŸ‘‰ Let's explore the Heroes of Domain-Driven Design blogpost series

  • There you will get familiar with the whole Software Development process: from knowledge crunching with domain experts, designing solution using Event Modeling, to implementation using DDD Building Blocks.

This project probably won't be fully-functional HOMM3 engine implementation, because it's done for educational purposes. If you'd like to talk with me about mentioned development practices fell free to contact on linkedin.com/in/mateusznakodach/.

I'm focused on domain modeling on the backend, but I've also played around with Rails app frontend using Hotwire.

Heroes3_CreatureRecruitment_ExampleGif

πŸš€ How to run the project locally?

  1. cd heroesofddd_rails_application
  2. docker compose up
  3. bundle install
  4. rails db:drop db:create db:migrate db:seed - (re)creates database and seed with example data
  5. rails server

Example use case:

🧱 Modules

Modules (mostly designed using Bounded Context heuristic) are designed and documented on EventModeling below. Each slice in a module is in certain color which shows the progress:

  • green -> completed
  • yellow -> implementation in progress
  • red -> to do
  • grey -> design in progress

List of modules you can see in lib/heroes directory of the Rails application.

heroes/
β”œβ”€β”€ astrologers
β”œβ”€β”€ calendar
β”œβ”€β”€ creature_recruitment

Each domain-focused module follows Vertical-Slice Architecture of three possible types: write, read and automation following Event Modeling nomenclature. Aggregates are implemented using Decider pattern.

πŸ‘Ύ Creature Recruitment

EventModeling_Module_CreatureRecruitment.png

Slices:

Aggregates:

πŸ§™ Astrologers

EventModeling_Module_Astrologers.png

Slices:

Aggregates:

πŸ“… Calendar

EventModeling_Module_Calendar.png

Slices:

Aggregates:

πŸ€– Working with AI Large Language Models:

If you'd like to use the whole source code as your prompt context generate codebase file by: npx ai-digest --whitespace-removal

Domain Model purity

Domain Events are decoupled from infrastructure RailsEventStore events. Every domain event is registered with corresponding functions which maps from domain to storage structure and vice-versa (as shown below).

WeekSymbolProclaimed = Class.new(RubyEventStore::Event) do
    def self.from_domain(domain_event)
      ::EventStore::Heroes::Astrologers::WeekSymbolProclaimed.new(
        data: {
          month: domain_event.month,
          week: domain_event.week,
          week_of: domain_event.week_of,
          growth: domain_event.growth
        }
      )
    end
    
    def self.to_domain(store_event)
      data = store_event.data.deep_symbolize_keys
      ::Heroes::Astrologers::WeekSymbolProclaimed.new(
        month: data[:month],
        week: data[:week],
        week_of: data[:week_of],
        growth: data[:growth],
      )
    end
end

πŸ§ͺ Testing

Tests using Real postgres Event Store, follows the approach:

  • write slice: given(events) -> when(command) -> then(events)
  • read slice: given(events) -> then(read model)
  • automation: when(event, state?) -> then(command)

Tests are focused on observable behavior which implicitly covers the DDD Aggregates, so the domain model can be refactored without changes in tests.

Example: write slice

EventModeling_GWT_TestCase_CreatureRecruitment.png

def test_given_dwelling_with_3_creature_when_recruit_2_creature_then_success
  # given
  given_domain_event(@stream_name, DwellingBuilt.new(@dwelling_id, @creature_id, @cost_per_troop))
  given_domain_event(@stream_name, AvailableCreaturesChanged.new(@dwelling_id, @creature_id, 3))

  # when
  recruit_creature = RecruitCreature.new(@dwelling_id, @creature_id, 2)
  execute_command(recruit_creature, @app_context)

  # then
  expected_cost = Heroes::SharedKernel::Resources::Cost.resources([ :GOLD, 6000 ], [ :GEM, 2 ])
  expected_event = CreatureRecruited.new(@dwelling_id, @creature_id, 2, expected_cost)
  then_domain_event(@stream_name, expected_event)
end

πŸ’Ό Hire me

If you'd like to hire me for Domain-Driven Design and/or Event Sourcing projects I'm available to work with: Kotlin, Java, C# .NET, Ruby and JavaScript/TypeScript (Node.js or React). Please reach me out on LinkedIn linkedin.com/in/mateusznakodach/.