Avoid premature abstraction
An important checklist item to take into account for the Groundwork project is to always carefully consider whether abstractions to the software need to be introduced. The architecture patterns used in the platform come in many different variations, and when searching information online they might seem to be requirements.
Anti-patterns taken from Avoiding Premature Software Abstractions (via Scribe.rip) by Jonas Tulstrup:
- Responsibilities are abstracted too granularly
- Design patterns are used without real benefit
- Performance is optimized prematurely
- Low coupling is introduced everywhere
Software literature likes to introduce abstractions. ONLY add them when absolutely necessary.
This diagram taken from the article says it all:
Align with Phoenix LiveView
In all investigation of the Elixir ecosystem it becomes ever clearer how instrumental and powerful the Phoenix Framework really is. Hence Phoenix will be leveraged to maximum extent. This poses challenges as Phoenix is an extensive framework with many features. The Mix code generator are considered learning tools only, and may not fit the desired architecture patterns.
Phoenix has the concept of Contexts since version 1.3, but they should not be confused with DDD Bounded Contexts. This is discussed on the Elixir Forum in this topic.
At ElixirConf 2021 Jenny Shih presented a great talk Context Driven Development: Architect your Code with Phoenix Context (video) and example implementation that combines Clean / Hexagonal architecture, DDD bounded contexts and Phoenix contexts into the project structure.
We will use Jenny’s work as a guidance for the clean architecture.
Elixir Umbrella app or not?
- The groundwork repository will be a monorepo for the core server and essential services (modeled as sub-domains).
- Service Modules will contain their own independent bounded context(s), and depend on the core server. They are implemented in their own separate repository.
- Umbrella Projects are frequently mentioned as a good separation for DDD Bounded Contexts.
- For the MVP an Umbrella project setup is not chosen. Triggered by advice by Saša Jurić (author of Elixir in Action), further investigation found What’s wrong with Umbrella Apps? discussing pros and cons.
- Instead Boundary is used to provide guidance to proper layer separation.
- However it looks like Umbrella Apps may be a better fit in the long run to separately model DDD Bounded Contexts and deploy Service Modules for system services that come bundled with Groundwork.
We apply the Clean Architecture pattern introduced by Uncle Bob. The pattern comes with an additional best-practice to create a descriptive top-level project folder structure, that is NOT organized along technical concepts, but instead reflects the domains being modeled.
Folder structure (example):
/lib ├– /groundwork | ├– /[BoundedContext] # internal | | ├– /model # domain | | | ├– [AnAggregate].ex # aggregate root | | | ├– [AnEntity]_entity.ex # entity | | | └– [AValueObject]_value.ex # value object | | | └– [AnAggregate]_store.ex # repository interface | | ├– /command # commands | | ├– /event # events | | ├– some_use_case.ex # use case | | ├– some_other_use_case.ex # ... | ├– router.ex | ├– event_store.ex | └– /[BoundedContext]_external # external └– /config
Here’s a module structure for an AccountManagement bounded context:
Groundwork.AccountManagement.Account Groundwork.AccountManagement.AccountStore Groundwork.AccountManagement.Command.CreateAccount Groundwork.AccountManagement.Event.AccountCreated Groundwork.AccountManagement.AccountManagement.create_account() # use case
Create apps using Mix with:
mix new app_name --module AppModuleName --sup
Commanded architecture considerations
Ben Smith, Commdanded maintainer provides insight on various Commanded application architectures:
- One monolithic service with a single global Commanded application.
- One monolithic service containing many contexts, all sharing a single Commanded application.
- One monolithic service containing many contexts, each context using its own Commanded application.
- Many microservices with each service using its own Commanded application, and an additional Commanded application for integration.
- Multi-tenant service with each tenant using its own Commanded Application.
For the Core Server and the Service Modules that represent system services, the first monolithic architecture design suffices. This is also the best starting point for the MVP.
When applications are developed on top of the Groundwork service platform, i.e. as independent Service Modules, then option 3 should be considered first.
Bootstrapping social experiences
There’s a tight relationship between Taskweave and Groundwork. The Process is not just a static documentation artifact with developer guidelines and reference material. In addition ever more of the Process will be supported by tools and automation. Domain driven design is foundational to creating social experiences with Solidground, and is an ongoing activity during the entire project development lifecycle. After all, domains are ever evolving, and the application should facilitate that.
From this perspective offering a bootstrap project to help developers is not ideal. A bootstrap can be used only once. As soon as it is customized there’s no way to regenerate boilerplate. This architecture decision is about adding a major component to the Solidground project: Floorplanner.
- Keep modeling in sync of the codebase
- Keep developers working on Solidground best-practices
- Keep Taskweave core to the development method
- Provide a baseline platform for automation tools
Event driven architecture
Event Driven Architecture (EDA) forms the basis of Groundwork. The bounded contexts of a domain model communicate via Domain events. Where these are implemented in different Service Modules or invoked remotely over the Fediverse message exchange takes place.
EDA being central means that all supported Architecture Patterns generate events, i.e. all bounded contexts whether they are implemented as Service Wrapper, CRUD, CQRS or CQRS/ES.
Floorplanner is operated via a Phoenix LiveView user interface, but functionality can also be invoked by the Floorplanner API.
Initially we focus on Phoenix LiveView UI. Headless operation support is on the roadmap.
See: housekeeping #19.
Uniform is an Elixir library that allows to develop a portfolio of apps as a monolith and then before deploying eject them into separate applications.
After investigating decision was to keep this Open before using the project, until more of the baseline for Floorplanner has been established. The Elixir library has potential, and the product concept of “Ejecting” for deployment fits well with Floorplanner’s social experience design.
- HexDocs: Uniform Use Cases
- HexDocs: Uniform Benefits and Advantages
- Elixir Forum: Uniform - Less boilerplate, more code reuse in your portfolio of Elixir apps