smart_domain 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/CHANGELOG.md +25 -0
  4. data/LICENSE +21 -0
  5. data/README.md +1219 -0
  6. data/Rakefile +12 -0
  7. data/examples/blog_app/.dockerignore +51 -0
  8. data/examples/blog_app/.github/dependabot.yml +12 -0
  9. data/examples/blog_app/.github/workflows/ci.yml +67 -0
  10. data/examples/blog_app/.gitignore +30 -0
  11. data/examples/blog_app/.kamal/hooks/docker-setup.sample +3 -0
  12. data/examples/blog_app/.kamal/hooks/post-app-boot.sample +3 -0
  13. data/examples/blog_app/.kamal/hooks/post-deploy.sample +14 -0
  14. data/examples/blog_app/.kamal/hooks/post-proxy-reboot.sample +3 -0
  15. data/examples/blog_app/.kamal/hooks/pre-app-boot.sample +3 -0
  16. data/examples/blog_app/.kamal/hooks/pre-build.sample +51 -0
  17. data/examples/blog_app/.kamal/hooks/pre-connect.sample +47 -0
  18. data/examples/blog_app/.kamal/hooks/pre-deploy.sample +122 -0
  19. data/examples/blog_app/.kamal/hooks/pre-proxy-reboot.sample +3 -0
  20. data/examples/blog_app/.kamal/secrets +20 -0
  21. data/examples/blog_app/.rubocop.yml +8 -0
  22. data/examples/blog_app/.ruby-version +1 -0
  23. data/examples/blog_app/Dockerfile +76 -0
  24. data/examples/blog_app/Gemfile +63 -0
  25. data/examples/blog_app/Gemfile.lock +408 -0
  26. data/examples/blog_app/README.md +24 -0
  27. data/examples/blog_app/README_EXAMPLE.md +328 -0
  28. data/examples/blog_app/Rakefile +6 -0
  29. data/examples/blog_app/app/assets/images/.keep +0 -0
  30. data/examples/blog_app/app/assets/stylesheets/application.css +10 -0
  31. data/examples/blog_app/app/controllers/api/base_controller.rb +61 -0
  32. data/examples/blog_app/app/controllers/api/v1/posts_controller.rb +158 -0
  33. data/examples/blog_app/app/controllers/api/v1/users_controller.rb +98 -0
  34. data/examples/blog_app/app/controllers/application_controller.rb +7 -0
  35. data/examples/blog_app/app/controllers/concerns/.keep +0 -0
  36. data/examples/blog_app/app/domains/.keep +0 -0
  37. data/examples/blog_app/app/domains/exceptions.rb +19 -0
  38. data/examples/blog_app/app/domains/post_management/events/post_created_event.rb +15 -0
  39. data/examples/blog_app/app/domains/post_management/events/post_deleted_event.rb +13 -0
  40. data/examples/blog_app/app/domains/post_management/events/post_updated_event.rb +13 -0
  41. data/examples/blog_app/app/domains/post_management/handlers/post_notification_handler.rb +33 -0
  42. data/examples/blog_app/app/domains/post_management/models/post.rb +21 -0
  43. data/examples/blog_app/app/domains/post_management/policies/post_policy.rb +49 -0
  44. data/examples/blog_app/app/domains/post_management/post_service.rb +93 -0
  45. data/examples/blog_app/app/domains/post_management/setup.rb +25 -0
  46. data/examples/blog_app/app/domains/user_management/events/user_created_event.rb +15 -0
  47. data/examples/blog_app/app/domains/user_management/events/user_deleted_event.rb +13 -0
  48. data/examples/blog_app/app/domains/user_management/events/user_updated_event.rb +13 -0
  49. data/examples/blog_app/app/domains/user_management/handlers/user_welcome_handler.rb +21 -0
  50. data/examples/blog_app/app/domains/user_management/models/user.rb +11 -0
  51. data/examples/blog_app/app/domains/user_management/policies/user_policy.rb +49 -0
  52. data/examples/blog_app/app/domains/user_management/setup.rb +25 -0
  53. data/examples/blog_app/app/domains/user_management/user_service.rb +93 -0
  54. data/examples/blog_app/app/events/.keep +0 -0
  55. data/examples/blog_app/app/events/application_event.rb +18 -0
  56. data/examples/blog_app/app/handlers/.keep +0 -0
  57. data/examples/blog_app/app/helpers/application_helper.rb +2 -0
  58. data/examples/blog_app/app/javascript/application.js +3 -0
  59. data/examples/blog_app/app/javascript/controllers/application.js +9 -0
  60. data/examples/blog_app/app/javascript/controllers/hello_controller.js +7 -0
  61. data/examples/blog_app/app/javascript/controllers/index.js +4 -0
  62. data/examples/blog_app/app/jobs/application_job.rb +7 -0
  63. data/examples/blog_app/app/mailers/application_mailer.rb +4 -0
  64. data/examples/blog_app/app/models/application_record.rb +3 -0
  65. data/examples/blog_app/app/models/concerns/.keep +0 -0
  66. data/examples/blog_app/app/models/organization.rb +6 -0
  67. data/examples/blog_app/app/policies/.keep +0 -0
  68. data/examples/blog_app/app/policies/application_policy.rb +23 -0
  69. data/examples/blog_app/app/services/.keep +0 -0
  70. data/examples/blog_app/app/services/application_service.rb +24 -0
  71. data/examples/blog_app/app/views/layouts/application.html.erb +29 -0
  72. data/examples/blog_app/app/views/layouts/mailer.html.erb +13 -0
  73. data/examples/blog_app/app/views/layouts/mailer.text.erb +1 -0
  74. data/examples/blog_app/app/views/pwa/manifest.json.erb +22 -0
  75. data/examples/blog_app/app/views/pwa/service-worker.js +26 -0
  76. data/examples/blog_app/bin/brakeman +7 -0
  77. data/examples/blog_app/bin/bundler-audit +6 -0
  78. data/examples/blog_app/bin/ci +6 -0
  79. data/examples/blog_app/bin/dev +2 -0
  80. data/examples/blog_app/bin/docker-entrypoint +8 -0
  81. data/examples/blog_app/bin/importmap +4 -0
  82. data/examples/blog_app/bin/jobs +6 -0
  83. data/examples/blog_app/bin/kamal +27 -0
  84. data/examples/blog_app/bin/rails +4 -0
  85. data/examples/blog_app/bin/rake +4 -0
  86. data/examples/blog_app/bin/rubocop +8 -0
  87. data/examples/blog_app/bin/setup +35 -0
  88. data/examples/blog_app/bin/thrust +5 -0
  89. data/examples/blog_app/config/application.rb +52 -0
  90. data/examples/blog_app/config/boot.rb +4 -0
  91. data/examples/blog_app/config/bundler-audit.yml +5 -0
  92. data/examples/blog_app/config/cable.yml +17 -0
  93. data/examples/blog_app/config/cache.yml +16 -0
  94. data/examples/blog_app/config/ci.rb +19 -0
  95. data/examples/blog_app/config/credentials.yml.enc +1 -0
  96. data/examples/blog_app/config/database.yml +41 -0
  97. data/examples/blog_app/config/deploy.yml +120 -0
  98. data/examples/blog_app/config/environment.rb +5 -0
  99. data/examples/blog_app/config/environments/development.rb +78 -0
  100. data/examples/blog_app/config/environments/production.rb +90 -0
  101. data/examples/blog_app/config/environments/test.rb +53 -0
  102. data/examples/blog_app/config/importmap.rb +7 -0
  103. data/examples/blog_app/config/initializers/active_domain.rb +37 -0
  104. data/examples/blog_app/config/initializers/assets.rb +7 -0
  105. data/examples/blog_app/config/initializers/content_security_policy.rb +29 -0
  106. data/examples/blog_app/config/initializers/filter_parameter_logging.rb +8 -0
  107. data/examples/blog_app/config/initializers/inflections.rb +16 -0
  108. data/examples/blog_app/config/locales/en.yml +31 -0
  109. data/examples/blog_app/config/master.key +1 -0
  110. data/examples/blog_app/config/puma.rb +42 -0
  111. data/examples/blog_app/config/queue.yml +18 -0
  112. data/examples/blog_app/config/recurring.yml +15 -0
  113. data/examples/blog_app/config/routes.rb +27 -0
  114. data/examples/blog_app/config/storage.yml +27 -0
  115. data/examples/blog_app/config.ru +6 -0
  116. data/examples/blog_app/db/cable_schema.rb +11 -0
  117. data/examples/blog_app/db/cache_schema.rb +12 -0
  118. data/examples/blog_app/db/migrate/20251230112502_create_organizations.rb +9 -0
  119. data/examples/blog_app/db/migrate/20251230112503_create_users.rb +12 -0
  120. data/examples/blog_app/db/migrate/20251230112504_create_posts.rb +14 -0
  121. data/examples/blog_app/db/queue_schema.rb +129 -0
  122. data/examples/blog_app/db/schema.rb +46 -0
  123. data/examples/blog_app/db/seeds.rb +9 -0
  124. data/examples/blog_app/lib/api_demo.rb +175 -0
  125. data/examples/blog_app/lib/demo.rb +150 -0
  126. data/examples/blog_app/lib/tasks/.keep +0 -0
  127. data/examples/blog_app/log/.keep +0 -0
  128. data/examples/blog_app/public/400.html +135 -0
  129. data/examples/blog_app/public/404.html +135 -0
  130. data/examples/blog_app/public/406-unsupported-browser.html +135 -0
  131. data/examples/blog_app/public/422.html +135 -0
  132. data/examples/blog_app/public/500.html +135 -0
  133. data/examples/blog_app/public/icon.png +0 -0
  134. data/examples/blog_app/public/icon.svg +3 -0
  135. data/examples/blog_app/public/robots.txt +1 -0
  136. data/examples/blog_app/script/.keep +0 -0
  137. data/examples/blog_app/storage/.keep +0 -0
  138. data/examples/blog_app/tmp/.keep +0 -0
  139. data/examples/blog_app/vendor/.keep +0 -0
  140. data/examples/blog_app/vendor/javascript/.keep +0 -0
  141. data/lib/generators/active_domain/domain/domain_generator.rb +116 -0
  142. data/lib/generators/active_domain/domain/templates/events/created_event.rb.tt +15 -0
  143. data/lib/generators/active_domain/domain/templates/events/deleted_event.rb.tt +13 -0
  144. data/lib/generators/active_domain/domain/templates/events/updated_event.rb.tt +13 -0
  145. data/lib/generators/active_domain/domain/templates/policy.rb.tt +49 -0
  146. data/lib/generators/active_domain/domain/templates/service.rb.tt +93 -0
  147. data/lib/generators/active_domain/domain/templates/setup.rb.tt +27 -0
  148. data/lib/generators/active_domain/install/install_generator.rb +58 -0
  149. data/lib/generators/active_domain/install/templates/README +28 -0
  150. data/lib/generators/active_domain/install/templates/application_event.rb +18 -0
  151. data/lib/generators/active_domain/install/templates/application_policy.rb +23 -0
  152. data/lib/generators/active_domain/install/templates/application_service.rb +24 -0
  153. data/lib/generators/active_domain/install/templates/initializer.rb +37 -0
  154. data/lib/smart_domain/configuration.rb +97 -0
  155. data/lib/smart_domain/domain/exceptions.rb +164 -0
  156. data/lib/smart_domain/domain/policy.rb +215 -0
  157. data/lib/smart_domain/domain/service.rb +230 -0
  158. data/lib/smart_domain/event/adapters/memory.rb +110 -0
  159. data/lib/smart_domain/event/base.rb +176 -0
  160. data/lib/smart_domain/event/handler.rb +98 -0
  161. data/lib/smart_domain/event/mixins.rb +156 -0
  162. data/lib/smart_domain/event/registration.rb +136 -0
  163. data/lib/smart_domain/generators/domain_generator.rb +4 -0
  164. data/lib/smart_domain/generators/install_generator.rb +4 -0
  165. data/lib/smart_domain/handlers/audit_handler.rb +216 -0
  166. data/lib/smart_domain/handlers/metrics_handler.rb +104 -0
  167. data/lib/smart_domain/integration/active_record.rb +169 -0
  168. data/lib/smart_domain/integration/multi_tenancy.rb +115 -0
  169. data/lib/smart_domain/railtie.rb +62 -0
  170. data/lib/smart_domain/tasks/domains.rake +43 -0
  171. data/lib/smart_domain/version.rb +5 -0
  172. data/lib/smart_domain.rb +77 -0
  173. data/smart_domain.gemspec +53 -0
  174. metadata +391 -0
data/README.md ADDED
@@ -0,0 +1,1219 @@
1
+ # SmartDomain
2
+
3
+ **Domain-Driven Design and Event-Driven Architecture for Rails**
4
+
5
+ SmartDomain brings battle-tested DDD/EDA patterns from platform to Ruby on Rails applications. It provides domain events, an event bus, generic handlers, and Rails generators for rapid domain scaffolding.
6
+
7
+ ## Features
8
+
9
+ - ✅ **Domain Events** - Immutable domain events with type safety
10
+ - ✅ **Event Bus** - Publish-subscribe pattern for decoupled communication
11
+ - ✅ **Event Mixins** - Reusable event fields (Actor, Audit, ChangeTracking, SecurityContext, Reason)
12
+ - ✅ **Generic Handlers** - 70% boilerplate reduction for audit and metrics
13
+ - ✅ **Rails Integration** - Seamless integration with ActiveRecord and Rails ecosystem
14
+ - ✅ **Rails Generators** - Scaffold complete domains with one command (coming soon)
15
+ - ✅ **Multi-tenancy Support** - Built-in support for multi-tenant applications
16
+ - ✅ **Audit Compliance** - Automatic audit logging for compliance requirements
17
+
18
+ ## Installation
19
+
20
+ Add this line to your application's Gemfile:
21
+
22
+ ```ruby
23
+ gem 'smart_domain'
24
+ ```
25
+
26
+ And then execute:
27
+
28
+ ```bash
29
+ bundle install
30
+ ```
31
+
32
+ Run the install generator:
33
+
34
+ ```bash
35
+ rails generate smart_domain:install
36
+ ```
37
+
38
+ This creates:
39
+ - `config/initializers/smart_domain.rb` - Configuration file
40
+ - `app/events/application_event.rb` - Base event class
41
+ - `app/policies/application_policy.rb` - Base policy class
42
+ - `app/services/application_service.rb` - Base service class
43
+ - `app/domains/` - Domain directory structure
44
+
45
+ ## Quick Start
46
+
47
+ ### 1. Configure SmartDomain
48
+
49
+ Create an initializer in `config/initializers/smart_domain.rb`:
50
+
51
+ ```ruby
52
+ SmartDomain.configure do |config|
53
+ config.event_bus_adapter = :memory # or :redis, :active_job
54
+ config.audit_table_enabled = true # Enable audit table writes
55
+ config.multi_tenancy_enabled = true # Enable multi-tenancy
56
+ config.tenant_key = :organization_id
57
+ config.async_handlers = false # Use ActiveJob for async handlers
58
+ config.logger = Rails.logger
59
+ end
60
+ ```
61
+
62
+ ### 2. Define Domain Events
63
+
64
+ ```ruby
65
+ # app/events/user_created_event.rb
66
+ class UserCreatedEvent < SmartDomain::Event::Base
67
+ attribute :user_id, :string
68
+ attribute :email, :string
69
+
70
+ validates :user_id, :email, presence: true
71
+ end
72
+
73
+ # app/events/user_updated_event.rb
74
+ class UserUpdatedEvent < SmartDomain::Event::Base
75
+ include SmartDomain::Event::ActorMixin # Who
76
+ include SmartDomain::Event::ChangeTrackingMixin # What changed
77
+
78
+ attribute :user_id, :string
79
+
80
+ validates :user_id, presence: true
81
+ end
82
+ ```
83
+
84
+ ### 3. Publish Events
85
+
86
+ ```ruby
87
+ # In your service or model
88
+ user = User.create!(email: 'user@example.com', name: 'John Doe')
89
+
90
+ event = UserCreatedEvent.new(
91
+ event_type: 'user.created',
92
+ aggregate_id: user.id,
93
+ aggregate_type: 'User',
94
+ organization_id: current_organization.id,
95
+ user_id: user.id,
96
+ email: user.email
97
+ )
98
+
99
+ SmartDomain::Event.bus.publish(event)
100
+ ```
101
+
102
+ ### 4. Register Event Handlers (The Magic!)
103
+
104
+ Instead of manually subscribing audit and metrics handlers to each event:
105
+
106
+ ```ruby
107
+ # ❌ OLD WAY - Lots of boilerplate
108
+ audit = AuditHandler.new('user')
109
+ metrics = MetricsHandler.new('user')
110
+ SmartDomain::Event.bus.subscribe('user.created', audit)
111
+ SmartDomain::Event.bus.subscribe('user.created', metrics)
112
+ SmartDomain::Event.bus.subscribe('user.updated', audit)
113
+ SmartDomain::Event.bus.subscribe('user.updated', metrics)
114
+ # ... repeat for all events ...
115
+ ```
116
+
117
+ Use this **one-liner** for 70% boilerplate reduction:
118
+
119
+ ```ruby
120
+ # ✅ NEW WAY - One line for all standard handlers
121
+ SmartDomain::Event::Registration.register_standard_handlers(
122
+ domain: 'user',
123
+ events: %w[created updated deleted activated suspended],
124
+ include_audit: true,
125
+ include_metrics: true
126
+ )
127
+ ```
128
+
129
+ ### 5. Create Custom Event Handlers
130
+
131
+ ```ruby
132
+ # app/handlers/user_email_handler.rb
133
+ class UserEmailHandler < SmartDomain::Event::Handler
134
+ def handle(event)
135
+ case event.event_type
136
+ when 'user.created'
137
+ UserMailer.welcome_email(event.user_id).deliver_later
138
+ when 'user.activated'
139
+ UserMailer.account_activated(event.user_id).deliver_later
140
+ end
141
+ end
142
+
143
+ def can_handle?(event_type)
144
+ ['user.created', 'user.activated'].include?(event_type)
145
+ end
146
+ end
147
+
148
+ # Register custom handler
149
+ email_handler = UserEmailHandler.new
150
+ SmartDomain::Event.bus.subscribe('user.created', email_handler)
151
+ SmartDomain::Event.bus.subscribe('user.activated', email_handler)
152
+ ```
153
+
154
+ ## Core Concepts
155
+
156
+ ### Domain Events
157
+
158
+ Domain events represent significant business occurrences in your application. They are:
159
+
160
+ - **Immutable** - Once created, events cannot be modified
161
+ - **Type-safe** - Uses ActiveModel::Attributes for type coercion
162
+ - **Validated** - ActiveModel validations ensure event integrity
163
+ - **Structured** - Standardized fields across all events
164
+
165
+ **Core Event Fields:**
166
+ - `event_id` - Unique event identifier (UUID)
167
+ - `event_type` - Event type (e.g., 'user.created')
168
+ - `aggregate_id` - ID of the entity that produced the event
169
+ - `aggregate_type` - Type of entity (e.g., 'User', 'Order')
170
+ - `organization_id` - Tenant/organization context
171
+ - `occurred_at` - Timestamp when event occurred
172
+ - `version` - Event schema version
173
+ - `correlation_id` - For tracing related events
174
+ - `causation_id` - Event that caused this event
175
+ - `metadata` - Additional data
176
+
177
+ ### Event Mixins
178
+
179
+ Event mixins provide reusable fields for common patterns:
180
+
181
+ #### ActorMixin - WHO performed the action
182
+ ```ruby
183
+ class UserSuspendedEvent < SmartDomain::Event::Base
184
+ include SmartDomain::Event::ActorMixin
185
+
186
+ # Provides: actor_id, actor_email
187
+ end
188
+ ```
189
+
190
+ #### ChangeTrackingMixin - WHAT changed
191
+ ```ruby
192
+ class UserUpdatedEvent < SmartDomain::Event::Base
193
+ include SmartDomain::Event::ChangeTrackingMixin
194
+
195
+ # Provides: changed_fields, old_values, new_values
196
+ end
197
+
198
+ # Usage
199
+ event = UserUpdatedEvent.new(
200
+ ...,
201
+ changed_fields: ['email', 'name'],
202
+ old_values: { email: 'old@example.com', name: 'Old Name' },
203
+ new_values: { email: 'new@example.com', name: 'New Name' }
204
+ )
205
+ ```
206
+
207
+ #### SecurityContextMixin - WHERE from
208
+ ```ruby
209
+ class UserLoggedInEvent < SmartDomain::Event::Base
210
+ include SmartDomain::Event::SecurityContextMixin
211
+
212
+ # Provides: ip_address, user_agent
213
+ end
214
+ ```
215
+
216
+ #### ReasonMixin - WHY
217
+ ```ruby
218
+ class UserBannedEvent < SmartDomain::Event::Base
219
+ include SmartDomain::Event::ReasonMixin
220
+
221
+ # Provides: reason
222
+ end
223
+
224
+ event = UserBannedEvent.new(
225
+ ...,
226
+ reason: 'Violation of terms of service - spam activity detected'
227
+ )
228
+ ```
229
+
230
+ ### Event Bus
231
+
232
+ The event bus follows the **publish-subscribe pattern** for decoupled communication.
233
+
234
+ **Features:**
235
+ - Synchronous by default (in-memory)
236
+ - Pluggable adapters (Memory, Redis, ActiveJob)
237
+ - Error isolation (handler failures don't affect other handlers)
238
+ - Structured logging
239
+
240
+ ### Generic Handlers
241
+
242
+ Generic handlers provide cross-cutting concerns for all domains:
243
+
244
+ #### AuditHandler
245
+ - Logs all events to Rails logger with structured data
246
+ - Optionally writes to audit_events table for compliance
247
+ - Categorizes events (authentication, data_access, admin_action, system_event)
248
+ - Assesses risk level (HIGH, MEDIUM, LOW)
249
+ - Extracts fields from event mixins (actor_id, ip_address, old_values, etc.)
250
+
251
+ #### MetricsHandler
252
+ - Collects metrics from domain events
253
+ - Integrates with metrics systems (StatsD, Datadog, Prometheus, CloudWatch)
254
+ - Provides metric name and tags for easy querying
255
+
256
+ ### Hybrid Event Approach
257
+
258
+ The hybrid approach combines explicit event definitions with generic infrastructure:
259
+
260
+ **Before (70 lines of repetitive code):**
261
+ ```ruby
262
+ class UserAuditHandler < SmartDomain::Event::Handler
263
+ def handle(event)
264
+ # Audit logging logic...
265
+ end
266
+ end
267
+
268
+ class UserMetricsHandler < SmartDomain::Event::Handler
269
+ def handle(event)
270
+ # Metrics logic...
271
+ end
272
+ end
273
+
274
+ # Manual subscription
275
+ audit = UserAuditHandler.new
276
+ metrics = UserMetricsHandler.new
277
+ bus.subscribe('user.created', audit)
278
+ bus.subscribe('user.updated', audit)
279
+ bus.subscribe('user.created', metrics)
280
+ bus.subscribe('user.updated', metrics)
281
+ # ... 50 more lines ...
282
+ ```
283
+
284
+ **After (1 line with `register_standard_handlers`):**
285
+ ```ruby
286
+ SmartDomain::Event::Registration.register_standard_handlers(
287
+ domain: 'user',
288
+ events: %w[created updated deleted],
289
+ include_audit: true,
290
+ include_metrics: true
291
+ )
292
+ ```
293
+
294
+ **Result:** 70% less boilerplate, validated in production at Aeyes!
295
+
296
+ ## Architecture Patterns
297
+
298
+ ### Domain-Driven Design (DDD)
299
+
300
+ SmartDomain encourages organizing code by **bounded contexts** (domains):
301
+
302
+ ```
303
+ app/domains/
304
+ ├── user_management/
305
+ │ ├── user_service.rb
306
+ │ ├── user_events.rb
307
+ │ ├── user_handlers.rb
308
+ │ ├── user_policy.rb
309
+ │ └── setup.rb
310
+ ├── order_management/
311
+ │ ├── order_service.rb
312
+ │ ├── order_events.rb
313
+ │ └── setup.rb
314
+ └── ...
315
+ ```
316
+
317
+ ### Event-Driven Architecture (EDA)
318
+
319
+ **Principles:**
320
+ 1. **Events are mandatory** - Every significant business action publishes an event
321
+ 2. **Publish after commit** - Events published after database transaction commits
322
+ 3. **Cross-domain communication via events** - Domains don't call each other directly
323
+ 4. **Fire-and-forget** - Event publishing is asynchronous from handling
324
+
325
+ **Example:**
326
+ ```ruby
327
+ # ✅ GOOD: Publish event
328
+ ActiveRecord::Base.transaction do
329
+ user = User.create!(params)
330
+ event = UserCreatedEvent.new(...)
331
+
332
+ # Publish AFTER commit
333
+ ActiveRecord::Base.connection.after_transaction_commit do
334
+ SmartDomain::Event.bus.publish(event)
335
+ end
336
+ end
337
+
338
+ # ❌ BAD: Direct cross-domain call
339
+ OrderService.cancel_user_orders(user.id) # Tight coupling!
340
+ ```
341
+
342
+ ## Configuration Options
343
+
344
+ ```ruby
345
+ SmartDomain.configure do |config|
346
+ # Event bus adapter (:memory, :redis, :active_job)
347
+ config.event_bus_adapter = :memory
348
+
349
+ # Enable automatic writes to audit_events table
350
+ config.audit_table_enabled = true
351
+
352
+ # Enable multi-tenancy support
353
+ config.multi_tenancy_enabled = true
354
+
355
+ # Key used for tenant identification
356
+ config.tenant_key = :organization_id
357
+
358
+ # Use ActiveJob for asynchronous event handling
359
+ config.async_handlers = false
360
+
361
+ # Logger instance
362
+ config.logger = Rails.logger
363
+ end
364
+ ```
365
+
366
+ ## Audit Table Schema
367
+
368
+ If `audit_table_enabled` is true, create an `audit_events` table:
369
+
370
+ ```ruby
371
+ create_table :audit_events do |t|
372
+ t.string :event_type, null: false
373
+ t.string :event_category, null: false
374
+ t.bigint :user_id
375
+ t.string :organization_id
376
+ t.string :ip_address
377
+ t.text :user_agent
378
+ t.json :old_values
379
+ t.json :new_values
380
+ t.datetime :occurred_at, null: false
381
+ t.string :risk_level
382
+ t.json :compliance_flags
383
+
384
+ t.timestamps
385
+
386
+ t.index :event_type
387
+ t.index :event_category
388
+ t.index :user_id
389
+ t.index :organization_id
390
+ t.index :occurred_at
391
+ end
392
+ ```
393
+
394
+ ## Testing
395
+
396
+ Testing events and handlers is straightforward:
397
+
398
+ ```ruby
399
+ # spec/support/event_helpers.rb
400
+ module EventHelpers
401
+ def published_events
402
+ @published_events ||= []
403
+ end
404
+
405
+ def expect_event(event_type)
406
+ expect(published_events.map(&:event_type)).to include(event_type)
407
+ end
408
+
409
+ def stub_event_bus
410
+ allow(SmartDomain::Event.bus).to receive(:publish) do |event|
411
+ published_events << event
412
+ end
413
+ end
414
+ end
415
+
416
+ RSpec.configure do |config|
417
+ config.include EventHelpers
418
+
419
+ config.before(:each) do
420
+ @published_events = []
421
+ stub_event_bus
422
+ end
423
+ end
424
+
425
+ # spec/services/user_service_spec.rb
426
+ RSpec.describe UserService do
427
+ it 'publishes user.created event' do
428
+ service = UserService.new
429
+ user = service.create_user(email: 'test@example.com')
430
+
431
+ expect(user).to be_persisted
432
+ expect_event('user.created')
433
+ end
434
+ end
435
+ ```
436
+
437
+ ## Domain Service Pattern
438
+
439
+ Domain services encapsulate business logic that doesn't naturally fit within a single entity. They follow these principles:
440
+
441
+ 1. **Services own business logic** - Controllers delegate to services
442
+ 2. **Services publish events** - After successful operations
443
+ 3. **Services use transactions** - For data consistency
444
+ 4. **Services are stateless** - Except for injected context
445
+
446
+ ### Creating a Domain Service
447
+
448
+ ```ruby
449
+ # app/services/user_service.rb
450
+ class UserService < SmartDomain::Domain::Service
451
+ def create_user(attributes)
452
+ # Validate input
453
+ raise ValidationError.new('Email is required') if attributes[:email].blank?
454
+
455
+ User.transaction do
456
+ # Check business rules
457
+ if User.exists?(email: attributes[:email])
458
+ raise AlreadyExistsError.new('User', 'email', attributes[:email])
459
+ end
460
+
461
+ # Create entity
462
+ user = User.create!(attributes)
463
+
464
+ # Build and publish event
465
+ event = build_event(UserCreatedEvent,
466
+ event_type: 'user.created',
467
+ aggregate_id: user.id,
468
+ aggregate_type: 'User',
469
+ user_id: user.id,
470
+ email: user.email
471
+ )
472
+
473
+ publish_after_commit(event)
474
+ user
475
+ end
476
+ end
477
+
478
+ def update_user(user_id, attributes)
479
+ user = User.find(user_id)
480
+
481
+ # Authorization
482
+ policy = UserPolicy.new(current_user, user)
483
+ authorize!(policy, :update?)
484
+
485
+ User.transaction do
486
+ user.update!(attributes)
487
+
488
+ # Extract changes for event
489
+ changes = extract_changes(user)
490
+
491
+ event = build_event(UserUpdatedEvent,
492
+ event_type: 'user.updated',
493
+ aggregate_id: user.id,
494
+ aggregate_type: 'User',
495
+ user_id: user.id,
496
+ **changes # Includes changed_fields, old_values, new_values
497
+ )
498
+
499
+ publish_after_commit(event)
500
+ user
501
+ end
502
+ end
503
+
504
+ def activate_user(user_id)
505
+ user = User.find(user_id)
506
+
507
+ # Business rule validation
508
+ unless user.pending?
509
+ raise InvalidStateError.new('User',
510
+ from: user.status,
511
+ to: 'active',
512
+ reason: 'User must be pending to activate'
513
+ )
514
+ end
515
+
516
+ User.transaction do
517
+ user.update!(status: 'active')
518
+
519
+ event = build_event(UserActivatedEvent,
520
+ event_type: 'user.activated',
521
+ aggregate_id: user.id,
522
+ aggregate_type: 'User',
523
+ user_id: user.id
524
+ )
525
+
526
+ publish_after_commit(event)
527
+ user
528
+ end
529
+ end
530
+ end
531
+ ```
532
+
533
+ ### Using Services in Controllers
534
+
535
+ ```ruby
536
+ class UsersController < ApplicationController
537
+ def create
538
+ service = UserService.new(
539
+ current_user: current_user,
540
+ organization_id: current_organization.id
541
+ )
542
+
543
+ @user = service.create_user(user_params)
544
+ redirect_to @user, notice: 'User created successfully'
545
+ rescue SmartDomain::Domain::ValidationError => e
546
+ flash[:error] = e.message
547
+ render :new
548
+ rescue SmartDomain::Domain::AlreadyExistsError => e
549
+ flash[:error] = e.message
550
+ render :new
551
+ end
552
+
553
+ def update
554
+ service = UserService.new(current_user: current_user)
555
+ @user = service.update_user(params[:id], user_params)
556
+ redirect_to @user, notice: 'User updated successfully'
557
+ rescue SmartDomain::Domain::UnauthorizedError
558
+ redirect_to root_path, alert: 'You are not authorized to perform this action'
559
+ end
560
+ end
561
+ ```
562
+
563
+ ### Service Helper Methods
564
+
565
+ The `Domain::Service` base class provides several helper methods:
566
+
567
+ #### `build_event(event_class, attributes)`
568
+ Automatically fills in `organization_id` and actor fields from service context:
569
+
570
+ ```ruby
571
+ event = build_event(UserCreatedEvent,
572
+ event_type: 'user.created',
573
+ aggregate_id: user.id,
574
+ aggregate_type: 'User',
575
+ user_id: user.id,
576
+ email: user.email
577
+ # organization_id, actor_id, actor_email filled automatically!
578
+ )
579
+ ```
580
+
581
+ #### `extract_changes(record)`
582
+ Extracts changed fields from ActiveRecord model for ChangeTrackingMixin:
583
+
584
+ ```ruby
585
+ user.update!(email: 'new@example.com')
586
+ changes = extract_changes(user)
587
+ # => {
588
+ # changed_fields: ['email'],
589
+ # old_values: { email: 'old@example.com' },
590
+ # new_values: { email: 'new@example.com' }
591
+ # }
592
+ ```
593
+
594
+ #### `authorize!(policy, action)`
595
+ Check authorization and raise error if not authorized:
596
+
597
+ ```ruby
598
+ policy = UserPolicy.new(current_user, user)
599
+ authorize!(policy, :update?) # Raises UnauthorizedError if not allowed
600
+ ```
601
+
602
+ #### `with_transaction(&block)`
603
+ Wrap operations in a database transaction:
604
+
605
+ ```ruby
606
+ with_transaction do
607
+ user = User.create!(attributes)
608
+ profile = Profile.create!(user: user, ...)
609
+ publish_after_commit(UserCreatedEvent.new(...))
610
+ end
611
+ ```
612
+
613
+ #### `log(level, message, data = {})`
614
+ Log with service context:
615
+
616
+ ```ruby
617
+ log(:info, 'User created', user_id: user.id, email: user.email)
618
+ ```
619
+
620
+ ## Domain Exceptions
621
+
622
+ SmartDomain provides a hierarchy of domain exceptions for business rule violations:
623
+
624
+ ```ruby
625
+ # Base exception
626
+ SmartDomain::Domain::Error
627
+
628
+ # Specific exceptions
629
+ SmartDomain::Domain::NotFoundError.new('User', user_id)
630
+ SmartDomain::Domain::AlreadyExistsError.new('User', 'email', email)
631
+ SmartDomain::Domain::BusinessRuleError.new('Cannot delete user with active orders')
632
+ SmartDomain::Domain::InvalidStateError.new('User', from: 'suspended', to: 'active')
633
+ SmartDomain::Domain::ValidationError.new('Validation failed', errors: { email: ['is required'] })
634
+ SmartDomain::Domain::UnauthorizedError.new('Not authorized')
635
+ SmartDomain::Domain::DependencyError.new('Redis')
636
+ ```
637
+
638
+ All exceptions support structured error details:
639
+
640
+ ```ruby
641
+ begin
642
+ service.create_user(params)
643
+ rescue SmartDomain::Domain::AlreadyExistsError => e
644
+ render json: e.to_h, status: :unprocessable_entity
645
+ # => {
646
+ # error: "SmartDomain::Domain::AlreadyExistsError",
647
+ # message: "User with email 'test@example.com' already exists",
648
+ # code: :already_exists,
649
+ # details: {
650
+ # entity_type: "User",
651
+ # attribute: "email",
652
+ # value: "test@example.com"
653
+ # }
654
+ # }
655
+ end
656
+ ```
657
+
658
+ ## Domain Policies (Authorization)
659
+
660
+ Domain policies encapsulate authorization logic (similar to Pundit):
661
+
662
+ ```ruby
663
+ # app/policies/user_policy.rb
664
+ class UserPolicy < SmartDomain::Domain::Policy
665
+ def create?
666
+ user.admin? || user.manager?
667
+ end
668
+
669
+ def update?
670
+ user.admin? || owner?
671
+ end
672
+
673
+ def destroy?
674
+ user.admin? && record.id != user.id
675
+ end
676
+
677
+ def activate?
678
+ user.admin? && record.pending?
679
+ end
680
+
681
+ class Scope < Scope
682
+ def resolve
683
+ if user.admin?
684
+ scope.all
685
+ else
686
+ scope.where(organization_id: user.organization_id)
687
+ end
688
+ end
689
+ end
690
+ end
691
+
692
+ # Usage in service
693
+ policy = UserPolicy.new(current_user, user)
694
+ authorize!(policy, :update?) # Raises UnauthorizedError if false
695
+
696
+ # Usage in controller (for index)
697
+ @users = policy_scope(User.all, UserPolicy)
698
+ ```
699
+
700
+ ## ActiveRecord Integration
701
+
702
+ SmartDomain provides seamless integration with ActiveRecord for publishing domain events directly from models.
703
+
704
+ ### Including in Models
705
+
706
+ ```ruby
707
+ class User < ApplicationRecord
708
+ include SmartDomain::Integration::ActiveRecord
709
+
710
+ after_create :publish_created_event
711
+ after_update :publish_updated_event
712
+ after_destroy :publish_deleted_event
713
+
714
+ private
715
+
716
+ def publish_created_event
717
+ event = build_domain_event(UserCreatedEvent,
718
+ event_type: 'user.created',
719
+ user_id: id,
720
+ email: email
721
+ )
722
+ add_domain_event(event)
723
+ end
724
+
725
+ def publish_updated_event
726
+ return if saved_changes.empty?
727
+
728
+ changes = domain_event_changes
729
+
730
+ event = build_domain_event(UserUpdatedEvent,
731
+ event_type: 'user.updated',
732
+ user_id: id,
733
+ **changes # changed_fields, old_values, new_values
734
+ )
735
+ add_domain_event(event)
736
+ end
737
+
738
+ def publish_deleted_event
739
+ event = build_domain_event(UserDeletedEvent,
740
+ event_type: 'user.deleted',
741
+ user_id: id,
742
+ email: email
743
+ )
744
+ add_domain_event(event)
745
+ end
746
+ end
747
+ ```
748
+
749
+ ### How It Works
750
+
751
+ 1. **Events are queued** during callbacks (after_create, after_update, etc.)
752
+ 2. **Events are published** AFTER the database transaction commits
753
+ 3. **If transaction rolls back**, events are automatically discarded
754
+ 4. **Thread-safe** - Each model instance has its own event queue
755
+
756
+ ### Helper Methods
757
+
758
+ #### `add_domain_event(event)`
759
+ Queue an event for publishing after commit:
760
+
761
+ ```ruby
762
+ event = UserCreatedEvent.new(...)
763
+ add_domain_event(event)
764
+ ```
765
+
766
+ #### `build_domain_event(event_class, attributes)`
767
+ Build an event with automatic field population:
768
+
769
+ ```ruby
770
+ # Automatically fills: aggregate_id, aggregate_type, organization_id
771
+ event = build_domain_event(UserCreatedEvent,
772
+ event_type: 'user.created',
773
+ user_id: id,
774
+ email: email
775
+ )
776
+ ```
777
+
778
+ #### `domain_event_changes`
779
+ Extract changes for ChangeTrackingMixin:
780
+
781
+ ```ruby
782
+ user.update!(email: 'new@example.com')
783
+
784
+ changes = user.domain_event_changes
785
+ # => {
786
+ # changed_fields: ['email', 'updated_at'],
787
+ # old_values: { email: 'old@example.com', ... },
788
+ # new_values: { email: 'new@example.com', ... }
789
+ # }
790
+ ```
791
+
792
+ ### Transaction Safety
793
+
794
+ Events are **only published if the transaction succeeds**:
795
+
796
+ ```ruby
797
+ User.transaction do
798
+ user = User.create!(email: 'test@example.com')
799
+ # Event queued, not published yet
800
+
801
+ raise ActiveRecord::Rollback # Transaction rolls back
802
+ # Event is discarded
803
+ end
804
+ # No event published!
805
+
806
+ User.transaction do
807
+ user = User.create!(email: 'test@example.com')
808
+ # Event queued
809
+ end # Transaction commits
810
+ # UserCreatedEvent published!
811
+ ```
812
+
813
+ ### Nested Transactions
814
+
815
+ Works correctly with nested transactions:
816
+
817
+ ```ruby
818
+ User.transaction do
819
+ user = User.create!(email: 'test@example.com')
820
+
821
+ User.transaction(requires_new: true) do
822
+ profile = Profile.create!(user: user)
823
+ # Both events queued
824
+ end # Inner transaction commits
825
+
826
+ end # Outer transaction commits
827
+ # Both events published here
828
+ ```
829
+
830
+ ## Multi-Tenancy Support
831
+
832
+ SmartDomain provides built-in multi-tenancy support with thread-safe tenant context.
833
+
834
+ ### Setting Up Multi-Tenancy
835
+
836
+ ```ruby
837
+ # config/initializers/smart_domain.rb
838
+ SmartDomain.configure do |config|
839
+ config.multi_tenancy_enabled = true
840
+ config.tenant_key = :organization_id # Your tenant column name
841
+ end
842
+ ```
843
+
844
+ ### Controller Integration
845
+
846
+ ```ruby
847
+ class ApplicationController < ActionController::Base
848
+ around_action :set_current_tenant
849
+
850
+ private
851
+
852
+ def set_current_tenant
853
+ SmartDomain::Integration::TenantContext.with_tenant(current_organization.id) do
854
+ yield
855
+ end
856
+ end
857
+ end
858
+ ```
859
+
860
+ ### Automatic Tenant Assignment
861
+
862
+ ```ruby
863
+ class User < ApplicationRecord
864
+ include SmartDomain::Integration::TenantScoped
865
+
866
+ # organization_id will be automatically set from TenantContext.current
867
+ end
868
+
869
+ # In controller
870
+ SmartDomain::Integration::TenantContext.with_tenant('org-123') do
871
+ user = User.create!(email: 'test@example.com')
872
+ # user.organization_id => 'org-123'
873
+ end
874
+ ```
875
+
876
+ ### Tenant Context API
877
+
878
+ ```ruby
879
+ # Get current tenant
880
+ tenant_id = SmartDomain::Integration::TenantContext.current
881
+
882
+ # Set current tenant
883
+ SmartDomain::Integration::TenantContext.current = 'org-123'
884
+
885
+ # Execute within tenant context
886
+ SmartDomain::Integration::TenantContext.with_tenant('org-123') do
887
+ # All operations use org-123 as tenant
888
+ end
889
+
890
+ # Check if tenant is set
891
+ SmartDomain::Integration::TenantContext.tenant_set? # => true/false
892
+
893
+ # Clear tenant
894
+ SmartDomain::Integration::TenantContext.clear!
895
+ ```
896
+
897
+ ## Complete Example
898
+
899
+ Here's a complete example combining all features:
900
+
901
+ ```ruby
902
+ # app/models/user.rb
903
+ class User < ApplicationRecord
904
+ include SmartDomain::Integration::ActiveRecord
905
+ include SmartDomain::Integration::TenantScoped
906
+
907
+ validates :email, presence: true, uniqueness: true
908
+
909
+ after_create :publish_created_event
910
+ after_update :publish_updated_event
911
+
912
+ private
913
+
914
+ def publish_created_event
915
+ event = build_domain_event(UserCreatedEvent,
916
+ event_type: 'user.created',
917
+ user_id: id,
918
+ email: email
919
+ )
920
+ add_domain_event(event)
921
+ end
922
+
923
+ def publish_updated_event
924
+ return if saved_changes.empty?
925
+
926
+ event = build_domain_event(UserUpdatedEvent,
927
+ event_type: 'user.updated',
928
+ user_id: id,
929
+ **domain_event_changes
930
+ )
931
+ add_domain_event(event)
932
+ end
933
+ end
934
+
935
+ # app/events/user_created_event.rb
936
+ class UserCreatedEvent < SmartDomain::Event::Base
937
+ include SmartDomain::Event::ActorMixin
938
+
939
+ attribute :user_id, :string
940
+ attribute :email, :string
941
+
942
+ validates :user_id, :email, presence: true
943
+ end
944
+
945
+ # app/events/user_updated_event.rb
946
+ class UserUpdatedEvent < SmartDomain::Event::Base
947
+ include SmartDomain::Event::ActorMixin
948
+ include SmartDomain::Event::ChangeTrackingMixin
949
+
950
+ attribute :user_id, :string
951
+
952
+ validates :user_id, presence: true
953
+ end
954
+
955
+ # app/services/user_service.rb
956
+ class UserService < SmartDomain::Domain::Service
957
+ def create_user(attributes)
958
+ # Validation
959
+ if User.exists?(email: attributes[:email])
960
+ raise SmartDomain::Domain::AlreadyExistsError.new('User', 'email', attributes[:email])
961
+ end
962
+
963
+ # Create user (events published automatically)
964
+ User.create!(attributes)
965
+ end
966
+
967
+ def update_user(user_id, attributes)
968
+ user = User.find(user_id)
969
+
970
+ # Authorization
971
+ policy = UserPolicy.new(current_user, user)
972
+ authorize!(policy, :update?)
973
+
974
+ # Update user (events published automatically)
975
+ user.update!(attributes)
976
+ user
977
+ end
978
+ end
979
+
980
+ # config/initializers/smart_domain.rb
981
+ SmartDomain.configure do |config|
982
+ config.event_bus_adapter = :memory
983
+ config.audit_table_enabled = true
984
+ config.multi_tenancy_enabled = true
985
+ config.tenant_key = :organization_id
986
+ config.logger = Rails.logger
987
+ end
988
+
989
+ # Setup event handlers
990
+ SmartDomain::Event::Registration.register_standard_handlers(
991
+ domain: 'user',
992
+ events: %w[created updated deleted],
993
+ include_audit: true,
994
+ include_metrics: true
995
+ )
996
+
997
+ # app/controllers/users_controller.rb
998
+ class UsersController < ApplicationController
999
+ around_action :set_current_tenant
1000
+
1001
+ def create
1002
+ service = UserService.new(
1003
+ current_user: current_user,
1004
+ organization_id: current_organization.id
1005
+ )
1006
+
1007
+ @user = service.create_user(user_params)
1008
+ redirect_to @user, notice: 'User created successfully'
1009
+ rescue SmartDomain::Domain::AlreadyExistsError => e
1010
+ flash[:error] = e.message
1011
+ render :new
1012
+ end
1013
+
1014
+ private
1015
+
1016
+ def set_current_tenant
1017
+ SmartDomain::Integration::TenantContext.with_tenant(current_organization.id) do
1018
+ yield
1019
+ end
1020
+ end
1021
+ end
1022
+ ```
1023
+
1024
+ ## Rails Generators
1025
+
1026
+ SmartDomain provides powerful generators to scaffold complete domains with one command.
1027
+
1028
+ ### Install Generator
1029
+
1030
+ ```bash
1031
+ rails generate smart_domain:install
1032
+ ```
1033
+
1034
+ Creates the initial SmartDomain structure in your Rails app:
1035
+ - Configuration initializer
1036
+ - Base classes (ApplicationEvent, ApplicationPolicy, ApplicationService)
1037
+ - Directory structure (app/domains/, app/events/, app/handlers/, app/policies/)
1038
+
1039
+ ### Domain Generator
1040
+
1041
+ ```bash
1042
+ rails generate smart_domain:domain User
1043
+ ```
1044
+
1045
+ Generates a complete domain structure:
1046
+
1047
+ ```
1048
+ app/domains/user_management/
1049
+ user_service.rb # Business logic
1050
+ setup.rb # Event handler registration
1051
+
1052
+ app/events/
1053
+ user_created_event.rb # Created event
1054
+ user_updated_event.rb # Updated event (with ChangeTrackingMixin)
1055
+ user_deleted_event.rb # Deleted event
1056
+
1057
+ app/policies/
1058
+ user_policy.rb # Authorization rules
1059
+ ```
1060
+
1061
+ **Generated Service** (`app/domains/user_management/user_service.rb`):
1062
+ - Complete CRUD operations (create, update, delete, list)
1063
+ - Authorization checks
1064
+ - Event publishing
1065
+ - Business rule validation examples
1066
+ - Policy scoping
1067
+
1068
+ **Generated Events** (`app/events/user_*_event.rb`):
1069
+ - UserCreatedEvent with ActorMixin
1070
+ - UserUpdatedEvent with ActorMixin + ChangeTrackingMixin
1071
+ - UserDeletedEvent with ActorMixin
1072
+
1073
+ **Generated Policy** (`app/policies/user_policy.rb`):
1074
+ - Authorization rules (index?, show?, create?, update?, destroy?)
1075
+ - Scope class for index queries
1076
+ - Helper methods (admin?, owner?, same_organization?)
1077
+
1078
+ **Generated Setup** (`app/domains/user_management/setup.rb`):
1079
+ - Automatic event handler registration
1080
+ - One-line registration for audit and metrics handlers
1081
+ - Examples for custom handlers
1082
+
1083
+ ### Generator Options
1084
+
1085
+ ```bash
1086
+ # Skip specific files
1087
+ rails generate smart_domain:domain Order --skip-service
1088
+ rails generate smart_domain:domain Product --skip-policy
1089
+ rails generate smart_domain:domain Invoice --skip-events
1090
+
1091
+ # Generate minimal domain
1092
+ rails generate smart_domain:domain Report --skip-service --skip-policy
1093
+ ```
1094
+
1095
+ ### Rake Tasks
1096
+
1097
+ ```bash
1098
+ # List all registered domains
1099
+ rake smart_domain:domains
1100
+
1101
+ # Reload domain setups
1102
+ rake smart_domain:reload
1103
+ ```
1104
+
1105
+ ### Example Workflow
1106
+
1107
+ ```bash
1108
+ # 1. Install SmartDomain
1109
+ rails generate smart_domain:install
1110
+
1111
+ # 2. Generate your first domain
1112
+ rails generate smart_domain:domain User
1113
+
1114
+ # 3. Create the User model
1115
+ rails generate model User email:string name:string organization:references
1116
+
1117
+ # 4. Add SmartDomain integration to the model
1118
+ # In app/models/user.rb:
1119
+ class User < ApplicationRecord
1120
+ include SmartDomain::Integration::ActiveRecord
1121
+ include SmartDomain::Integration::TenantScoped
1122
+
1123
+ after_create :publish_created_event
1124
+ after_update :publish_updated_event
1125
+
1126
+ private
1127
+
1128
+ def publish_created_event
1129
+ event = build_domain_event(UserCreatedEvent,
1130
+ event_type: 'user.created',
1131
+ user_id: id,
1132
+ email: email
1133
+ )
1134
+ add_domain_event(event)
1135
+ end
1136
+
1137
+ def publish_updated_event
1138
+ return if saved_changes.empty?
1139
+
1140
+ event = build_domain_event(UserUpdatedEvent,
1141
+ event_type: 'user.updated',
1142
+ user_id: id,
1143
+ **domain_event_changes
1144
+ )
1145
+ add_domain_event(event)
1146
+ end
1147
+ end
1148
+
1149
+ # 5. Use the service in your controller
1150
+ class UsersController < ApplicationController
1151
+ def create
1152
+ service = UserManagement::UserService.new(
1153
+ current_user: current_user,
1154
+ organization_id: current_organization.id
1155
+ )
1156
+
1157
+ @user = service.create_user(user_params)
1158
+ redirect_to @user, notice: 'User created successfully'
1159
+ rescue SmartDomain::Domain::AlreadyExistsError => e
1160
+ flash[:error] = e.message
1161
+ render :new
1162
+ end
1163
+ end
1164
+
1165
+ # 6. Restart Rails to load domain setup
1166
+ rails restart
1167
+ ```
1168
+
1169
+ ## Roadmap
1170
+
1171
+ - [x] Core event system (Base, Bus, Mixins, Handlers)
1172
+ - [x] Generic handlers (Audit, Metrics)
1173
+ - [x] Event registration helper (70% boilerplate reduction)
1174
+ - [x] Configuration DSL
1175
+ - [x] Domain service pattern
1176
+ - [x] Domain exceptions
1177
+ - [x] Domain policies (authorization)
1178
+ - [x] ActiveRecord integration (after_commit hooks)
1179
+ - [x] Multi-tenancy support
1180
+ - [x] Rails generators (`rails g smart_domain:domain User`)
1181
+ - [x] Railtie for automatic setup
1182
+ - [x] Rake tasks (list domains, reload setups)
1183
+ - [ ] Redis adapter
1184
+ - [ ] ActiveJob adapter
1185
+ - [ ] Example Rails application
1186
+ - [ ] Comprehensive test suite
1187
+ - [ ] Documentation site
1188
+
1189
+ ## Contributing
1190
+
1191
+ Bug reports and pull requests are welcome on GitHub at https://github.com/rachid/smart_domain.
1192
+
1193
+ ## License
1194
+
1195
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
1196
+
1197
+ ## Acknowledgments
1198
+
1199
+ **Architecture designed and battle-tested by:**
1200
+ - Rachid Al Maach (@rachid)
1201
+
1202
+ **Influenced by:**
1203
+ - Domain-Driven Design (Eric Evans)
1204
+ - Event-Driven Architecture patterns
1205
+ - Rails Event Store
1206
+ - Healthcare platform architecture
1207
+
1208
+ ## Support
1209
+
1210
+ For questions, issues, or feature requests, please:
1211
+ 1. Check the documentation
1212
+ 2. Search existing GitHub issues
1213
+ 3. Create a new issue with detailed information
1214
+
1215
+ ---
1216
+
1217
+ **Last Updated:** 2025-12-29
1218
+ **Version:** 0.1.0
1219
+ **Status:** Alpha - Core features implemented, generators and Rails integration coming soon