You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: elixir/phoenix/DOMAIN_RESOURCE_ACTION_ARCHITECTURE.md
+35-35Lines changed: 35 additions & 35 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,12 +1,12 @@
1
1
# Domain Resource Action Architecture
2
2
3
-
The Domain Resource Action is an architecture pattern where each module for the Business Logic layer represents a single action possible on a Resource of a Domain or SubDomain of a Domain. This means that each module for a Resource Action is responsible for only one action, therefore it can only contain one public method, and when necessary it's bang version, the function name ending with `!`, e.g. `read/1` and `read!/1`.
3
+
The Domain Resource Action is an architecture pattern where each module for the Business Logic layer represents a single action possible on a Resource of a Domain or SubDomain of a Domain. This means that each module for a Resource Action is responsible for only one action, therefore it can only contain one public method, and when necessary its bang version, the function name ending with `!`, e.g. `read/1` and `read!/1`.
4
4
5
-
All modules inside a Resource Action can only be accessed through the Resource API module from the web layer (LiveViews, Controllers, etc.), from other Domain Resources or from the same Resource. This reduces accidental complexity by avoiding direct coupling between modules across boundaries. This also makes very easy to refactor later the code because anything consuming the Business Logic is only aware of the API module for the Resource.
5
+
All modules inside a Resource Action can only be accessed through the Resource API module from the web layer (LiveViews, Controllers, etc.), from other Domain Resources or from the same Resource. This reduces accidental complexity by avoiding direct coupling between modules across boundaries. This also makes it very easy to refactor later the code because anything consuming the Business Logic is only aware of the API module for the Resource.
6
6
7
-
Each Resource Action is unit tested and the Resource API it's only tested via its doc-tests to ensure docs examples are in sync with the code and that each function can be invoked.
7
+
Each Resource Action is unit tested and the Resource API is only tested via its doc-tests to ensure docs examples are in sync with the code and that each function can be invoked.
8
8
9
-
Any project following this DOMAIN_RESOURCE_ACTION_ARCHITECTURE.md MUST strictly adhere to [1. Folder Structure](#1-folder-structure) and implement [2. Milliseconds Timestamps](#2-milliseconds-timestamps)] and [3. Binary IDs](#3-binary-ids) without making assumptions, in doubt always ask to the user for clarifications.
9
+
Any project following this DOMAIN_RESOURCE_ACTION_ARCHITECTURE.md MUST strictly adhere to [1. Folder Structure](#1-folder-structure) and implement [2. Milliseconds Timestamps](#2-milliseconds-timestamps) and [3. Binary IDs](#3-binary-ids) without making assumptions, when in doubt always ask the user for clarifications.
10
10
11
11
## 1. Folder Structure
12
12
@@ -86,7 +86,7 @@ lib
86
86
│ │ │ ├── change
87
87
│ │ │ │ └── change_warehouse_stock.ex
88
88
│ │ │ └── stock.ex
89
-
│ │ └── wharehouses_stocks_api.ex
89
+
│ │ └── warehouses_stocks_api.ex
90
90
│ └── shared_domains_api.ex
91
91
├── my_app_web
92
92
│ ├── live
@@ -115,10 +115,10 @@ Breaking down the partial folder structure example for an Online Shop:
NOTE: Both Domains and Resources may have a shared folder for actions that are shared or for other things that common to them. Shared folders don't have by default a schema, but they **MUST**have always the API module.
121
+
NOTE: Both Domains and Resources may have a shared folder for actions that are shared or for other things that are common to them. Shared folders don't have a schema by default, but they **MUST** always have the API module.
122
122
123
123
### 1.2 Patterns to Create Files and Directories
124
124
@@ -127,7 +127,7 @@ NOTE: Both Domains and Resources may have a shared folder for actions that are s
- Module: `MyApp.<DomainPlural>.<DomainPlural><ResourcePlural>API` (e.g., `MyApp.Catalogs.CatalogsProductsAPI`). Catalogs is the Domain, Categories the Resource, and API the Type of module.
130
+
- Module: `MyApp.<DomainPlural>.<DomainPlural><ResourcePlural>API` (e.g., `MyApp.Catalogs.CatalogsProductsAPI`). Catalogs is the Domain, Products the Resource, and API the Type of module.
131
131
132
132
#### 1.2.2 Domain Resource Schema
133
133
@@ -153,25 +153,25 @@ The Domain folder is located at `lib/my_app/<domain_plural>`, e.g. `lib/my_app/c
153
153
154
154
Module types:
155
155
156
-
-`API` - This files are the only way that Resources can be accessed, inclusive from inside the Resource itself. They define the public contract for each Resource on the Domain folder.
156
+
-`API` - These files are the only way that Resources can be accessed, including from inside the Resource itself. They define the public contract for each Resource on the Domain folder.
157
157
158
158
#### 1.3.2 Domain Resource Folder
159
159
160
160
The Domain Resource folder is located at `lib/my_app/<domain_plural>/<resource_plural>`, e.g. `lib/my_app/catalogs/products`:
161
161
162
162
Module types:
163
163
164
-
-`Ecto Schema` - This are the usual Ecto Schema module generated by the Phoenix code generators, e.g, `product.ex`.
164
+
-`Ecto Schema` - These are the usual Ecto Schema module generated by the Phoenix code generators, e.g, `product.ex`.
165
165
166
166
167
167
#### 1.3.3 Domain Resource Action Folder
168
168
169
-
The Domain Resource folder is located at`lib/my_app/<domain_plural>/<resource_plural>/<action>`, e.g. `lib/my_app/catalogs/products/update`:
169
+
The Domain Resource Action folder is located at
170
170
171
171
Module Types:
172
172
173
-
- For simple actions, like the ones generate by Phoenix code generators, a single module will suffice. For example: `update_category_product.ex`.
174
-
- For complex actions it may be wise to separate in at least into three modules:
173
+
- For simple actions, like the ones generated by Phoenix code generators, a single module will suffice. For example: `update_category_product.ex`.
174
+
- For complex actions it may be wise to separate at least into three modules:
175
175
-`Handler` - entrypoint module to coordinate the work being done.
176
176
-`Core` - for pure business logic without side effects.
177
177
-`Storage`, `Queues`, etc, - a dedicated module per type of communication with the external world.
@@ -184,15 +184,15 @@ Module Types:
184
184
Example for the file `catalogs_products_api.ex` in the folder structure example.
# Bear in mind that by doing this approach of direct cross boundary calls we are coupling this module with anything we interact with.
283
283
# This is often called accidental complexity via accidental coupling.
@@ -290,13 +290,13 @@ defmodule MyApp.Catalogs.Products.Update.UpdateCatalogProductHandler do
290
290
end
291
291
```
292
292
293
-
Calls to core modules from a Domain Resource Action Handler aren't limited to one. This means we can have more than one Core module per action for whatever we need to do in therms of:
293
+
Calls to core modules from a Domain Resource Action Handler aren't limited to one. This means we can have more than one Core module per action for whatever we need to do in terms of:
294
294
- business rules validation
295
295
- data transformation
296
296
- data enrichment
297
-
- anything else that as no side effects or communicates with the external world.
297
+
- anything else that has no side effects or communicates with the external world.
298
298
299
-
For example, the width clause on the above example would have some more calls to core modules:
299
+
For example, the with clause on the above example would have some more calls to core modules:
300
300
301
301
```elixir
302
302
with {:ok, _scope} <-CatalogsProductsAPI.allowed(scope, :update_catalog_product),
@@ -308,7 +308,7 @@ with {:ok, _scope} <- CatalogsProductsAPI.allowed(scope, :update_catalog_product
308
308
end
309
309
```
310
310
311
-
This split in several Core modules is useful in complex Business Domains, that have complex rules and data transformations/enrichment's and whatever else. When the Business Domain is straightforward and simple, then we may not even use a Core module if it doesn't make sense for the current Resource Action being handled.
311
+
This split in several Core modules is useful in complex Business Domains, that have complex rules and data transformations/enrichments and whatever else. When the Business Domain is straightforward and simple, then we may not even use a Core module if it doesn't make sense for the current Resource Action being handled.
312
312
313
313
##### 1.4.4.2 Domain Resource Action Core Module Example
314
314
@@ -366,7 +366,7 @@ This approach to create the Phoenix commands is **MANDATORY** to generate code f
366
366
367
367
#### 1.5.2 Fixing Routes with Domain and Resource
368
368
369
-
Every-time a code generator is used, that supports the option `--web`, it must be used in the format `-web <DomainPlural>.<ResourcePlural>`, e.g. `--web Accounts.Users`.
369
+
Everytime a code generator is used, that supports the option `--web`, it must be used in the format `-web <DomainPlural>.<ResourcePlural>`, e.g. `--web Accounts.Users`.
370
370
371
371
Unfortunately a small bug exists in the code generators and the routes will have the resource `users` duplicated, e.g. `http://example.com/accounts/users/users/register`, but instead it needs to be `http://example.com/accounts/users/register`.
372
372
@@ -396,25 +396,25 @@ You **MUST** follow this steps:
396
396
397
397
1. Rename `lib/my_app/catalogs/products.ex` to `lib/my_app/catalogs/catalogs_products_api.ex`
398
398
2. Update the module definition from `MyApp.Catalogs.Products` to `MyApp.Catalogs.CatalogsProductsAPI`.
399
-
3. Extract each function body from the new module `MyApp.Catalogs.CatalogsProductsAPI` into it's own module with only one public function, named after the action, without the resource name, at `lib/my_app/catalogs/products/<action>/<action>_catalog_product.ex`. For example: `lib/my_app/catalogs/products/create/create_catalog_product.ex` with a function named `create`. The `CatalogsProductsAPI` function header is kept, but its body its now only calling the new action module function, but without using `defdelegate`, otherwise we loose the API contract. You **MUST** also extract private functions like for the `broadcast` action and make them public. Any access from a module to a Domain Resource Action module needs to got through the API module, direct access is **FORBIDDEN**.
399
+
3. Extract each function body from the new module `MyApp.Catalogs.CatalogsProductsAPI` into its own module with only one public function, named after the action, without the resource name, at `lib/my_app/catalogs/products/<action>/<action>_catalog_product.ex`. For example: `lib/my_app/catalogs/products/create/create_catalog_product.ex` with a function named `create`. The `CatalogsProductsAPI` function header is kept, but its body is now only calling the new action module function, but without using `defdelegate`, otherwise we lose the API contract. You **MUST** also extract private functions like for the `broadcast` action and make them public. Any access from a module to a Domain Resource Action module needs to go through the API module, direct access is **FORBIDDEN**.
400
400
4. Update the tests for the now refactored `MyApp.Catalogs.Products` context to test instead `MyApp.Catalogs.CatalogsProductsAPI`. Rename the test file, Module name, and then replace each call to the context with a call to new API module.
401
-
5. Run `mix test` to ensure no test is broken after the refactor. If any test its broken fix it before proceeding.
401
+
5. Run `mix test` to ensure no test is broken after the refactor. If any test is broken fix it before proceeding.
402
402
403
403
#### 1.5.4 Accessing the Business Logic Layer from the Web Layer
404
404
405
-
Calls from the web layer, like from a live view or controller are only allowed to a Domain Resource API, that in the folder structure example would be to one of the modules defined at `catalogs_categories_api.ex`, `catalogs_products_api.ex` and `wharehouse_stocks_api.ex`. This means that the usual calls to the context need to be replaced with calls to the Domain Resource API. For example, replacing `Catalogs.update_product` with `CatalogsProductsAPI.update_product`. The same needs to be done in the respective tests.
405
+
Calls from the web layer, like from a live view or controller are only allowed to a Domain Resource API, that in the folder structure example would be to one of the modules defined at `catalogs_categories_api.ex`, `catalogs_products_api.ex` and `warehouse_stocks_api.ex`. This means that the usual calls to the context need to be replaced with calls to the Domain Resource API. For example, replacing `Catalogs.update_product` with `CatalogsProductsAPI.update_product`. The same needs to be done in the respective tests.
406
406
407
407
Both a live view and a controller must only have logic to deal with web layer concerns, which usually consists in calling a Domain Resource API with the parameters of the request mapped to existing atoms, and deal with the returned result to decide if the web layers succeeds or fails the response it needs to send back.
408
408
409
409
##### 1.5.4.1 Atomized Attributes
410
410
411
-
The use of atomized attributes its not required to be introduced to existing code, but its recommend that at some point to refactor the existing code to use them, if not already done.
411
+
The use of atomized attributes is not required to be introduced to existing code, but it's recommended that at some point to refactor the existing code to use them, if not already done.
412
412
413
-
When this Architecture pattern is analyzed for the first time by an AI Coding Agent its recommended for it to check if the project is already using atomized parameters to call the Business Logic layer, and if not then ask the developer if he wants to use PLANNING.md to create an Intent with the tasks to implement it, or if he wants to do it himself. The Intent **MUST** be created as specified by the INTENT_SPECIFICATION.md and exemplified by the INTENT_EXAMPLE.md.
413
+
When this Architecture pattern is analyzed for the first time by an AI Coding Agent it's recommended for it to check if the project is already using atomized parameters to call the Business Logic layer, and if not then ask the developer if he wants to use PLANNING.md to create an Intent with the tasks to implement it, or if he wants to do it himself. The Intent **MUST** be created as specified by the INTENT_SPECIFICATION.md and exemplified by the INTENT_EXAMPLE.md.
414
414
415
415
##### 1.5.4.2 Example of calling the a Domain Resource API from LiveView
416
416
417
-
LiveVew mode trimmed to show only the code for the `edit` handle event, that's enough to illustrate the call to the Domain Resource API with atomized attributes:
417
+
LiveView mode trimmed to show only the code for the `edit` handle event, that's enough to illustrate the call to the Domain Resource API with atomized attributes:
418
418
419
419
420
420
```elixir
@@ -492,17 +492,17 @@ end
492
492
493
493
##### 1.5.4.4 Example to atomize and sanitize attributes
494
494
495
-
A better approach would be to use a dedicated struct for each Domain Resource accept input from the external world that atomizes the request input parameters into existing atoms and sanitizes their values.
495
+
A better approach would be to use a dedicated struct for each Domain Resource to accept input from the external world that atomizes the request input parameters into existing atoms and sanitizes their values.
#THe struct **MUST** explicitly enforce the required attributes.
501
+
#The struct **MUST** explicitly enforce the required attributes.
502
502
503
503
defsanitize_product_params(input_params) do
504
504
# For each atom key in the struct we want to sanitize the input_params value
505
-
# present in the correspondent string key.
505
+
# present in the corresponding string key.
506
506
507
507
# You may want to just let it crash if the required keys aren't all present.
508
508
@@ -514,7 +514,7 @@ end
514
514
515
515
## 2. Milliseconds Timestamps
516
516
517
-
By default Phoenix generates schemas and migration with timestamps in `:utc_datetime` which have a default precision of seconds, thus making impossible to order records in database queries by the insert or update times, because several records can be inserted or updated in the same second.
517
+
By default Phoenix generates schemas and migrations with timestamps in `:utc_datetime` which have a default precision of seconds, thus making impossible to order records in database queries by the insert or update times, because several records can be inserted or updated in the same second.
518
518
519
519
The solution is to modify in `config/config.exs`, the `generators` configuration to use `timestamp_type: :utc_datetime_usec` and the code generators will use them by default when creating migrations and schemas.
520
520
@@ -527,7 +527,7 @@ When this Architecture pattern is analyzed for the first time by an AI Coding Ag
527
527
528
528
For security reasons this architecture requires that all migrations and schemas use binary IDs for the primary key and foreign keys.
529
529
530
-
UUIDV7 as binary IDS are strongly recommended because they can be sorted in database queries, once they are time based.
530
+
UUIDV7 as binary IDs are strongly recommended because they can be sorted in database queries, once they are time based.
531
531
532
532
When this Architecture pattern is analyzed for the first by an AI Coding Agent it **MUST** check `config/config.exs` to see if the project is already using binary IDs and if not then ask the developer if he wants to use PLANNING.md to create an Intent with the tasks to implement it, or if he wants to do it himself. The Intent **MUST** be created as specified by the INTENT_SPECIFICATION.md and exemplified by the INTENT_EXAMPLE.md.
0 commit comments