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.
cd heroesofddd_rails_application
docker compose up
bundle install
rails db:drop db:create db:migrate db:seed
- (re)creates database and seed with example datarails server
- Go to the url: http://127.0.0.1:3000/heroes/games/fcc8f601-76cb-4b5a-972d-b7431303f69a/creature_recruitment/dwellings/cecc4307-e940-4ef2-8436-80c475729938
- Recruit Angels and click "Next day" in order to wait for the astrologers proclamation of the week symbol.
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.
Slices:
- Write: BuildDwelling -> DwellingBuilt | test
- Write: IncreaseAvailableCreatures -> AvailableCreaturesChanged | test
- Write: RecruitCreature -> CreatureRecruited | test
- Read: (DwellingBuilt, AvailableCreaturesChanged, CreatureRecruited) -> DwellingReadModel | test
Aggregates:
Slices:
- Write: ProclaimWeekSymbol -> WeekSymbolProclaimed | test
- Automation: DayStarted(where day==1) -> ProclaimWeekSymbol | test
- Automation: (WeekSymbolProclaimed, all game dwellings derived from DwellingBuilt events) -> IncreaseAvailableCreatures for each dwelling in the game where creature == symbol | test
Aggregates:
Slices:
- Write: StartDay -> DayStarted | test
- Write: FinishDay -> DayFinished | test
- Read: DayStarted -> CurrentDateReadModel | test
Aggregates:
If you'd like to use the whole source code as your prompt context generate codebase file by:
npx ai-digest --whitespace-removal
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
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.
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
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/.