Guiding Principles for SoC and DI
This page focuses on several implementation choices that helps to implement SoC and DI.
1. How to separate concerns
Deciding how to separate components and which layers to include within each component is a complex task that requires thoughtful design by the software architect. Some principles can guide this process:
- Concern-oriented design: identify the natural boundaries of a component by focusing on the key capability that is realized by the component. Often, a solution requires multiple objects to be changed, validated, and committed together. The involved objects and their associated logic help define the natural boundaries of a component. For more on this, see Adding atomicity, which explores this concept in detail.
- Reusability: if certain functionality is used across multiple parts of the application, it may be a good candidate for extraction into a reusable component. This promotes consistency and reduces duplication throughout the system.
- Keep Microflows Focused on a Single Task: A sign that a microflow is doing too much is when its description requires the word "and".
- Encapsulate Internal Logic: Expose public orchestration
ORCor page actionACTmicroflows as interfaces for your component and use private microflows to hide implementation details. - Avoid Cross-Component Domain Logic Calls: Do not call Domain Layer microflows across components. Instead, expose public ORC microflows that act as stable interfaces. This prevents tight coupling, which is a major cause of brittle test suites.
2. Guiding principles for Dependency Injection
- Centralize Retrieval Logic: Retrieve data with (nested) getter microflows (
GET) in order to hide the data structure of the domain model. This separates data access from business logic and allows both to be tested independently.
A nested getter microflow example is GET_M_orders_by_customer. It first uses GET_M_contract_by_customer to retrieve the customer’s contract, then calls GET_M_orders_by_contract to fetch all orders linked to that contract.
Be aware that when you retrieve an object from the database, the Mendix Runtime does not automatically merge it with an object that was retrieved earlier. This can result in two separate versions of the same object, and if both versions are changed, only the changes from the version that is committed last will be saved to the database. Any changes made to the first version will be lost.
When a nanoflow retrieves an object from database, Mendix checks if that object is already in the Client Cache. If so, Mendix will return the latest version from the Client Cache instead of fetching it from the database.
- Pass Data as Parameters. A microflow should "ask" for the data it needs via input parameters instead of "looking" for it with retrieve activities. This is the Mendix equivalent of Dependency Injection for data. It makes the microflow's dependencies explicit and allows you to easily provide test data (or "test doubles") without needing to set up a specific database state.
- Avoid Repeatedly Retrieving Global Settings. Do not repeatedly retrieve system-wide configuration objects from the database within your microflows, as this creates a dependency on a global state which complicates testing. Retrieve these settings before the microflow is called and pass the relevant configuration values as parameters to the microflow that need them.
If a microflow starts to have too many parameters, you can group the necessary data into a non-persistent configuration object. This lets you expand the logic without changing the microflow's interface.