Skip to content

Commit 64dc6b7

Browse files
committed
Correctly describe Upcast approach instead of Default mapper.
1 parent 11f56dd commit 64dc6b7

File tree

1 file changed

+45
-36
lines changed

1 file changed

+45
-36
lines changed

posts/2025-09-12-evolving-event-names-and-payloads-in-rails-event-store-without-breaking-history.md

Lines changed: 45 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -159,56 +159,65 @@ As described in [this post](https://blog.arkency.com/4-strategies-when-you-need-
159159

160160
After investigating its capabilities, we discovered that upcast can indeed handle both event class name changes and payload transformation through lambda functions. However, we chose to stick with our custom mapper approach for several practical reasons:
161161

162-
### Pipeline integration complexity
162+
### Excessive boilerplate when using lambdas
163+
164+
Rails Event Store provides `Transformation::Upcast` which can handle both event class name changes and payload transformation through lambda functions. After investigating its capabilities, we chose to stick with our custom mapper approach for several practical reasons:
163165

164-
RES upcast works beautifully as a standalone solution, but doesn't integrate cleanly with the transformation pipeline we needed:
166+
### Code organization and maintainability
167+
168+
While `Transformation::Upcast` is pipeline-compatible and would work with our transformation stack:
165169

166170
```ruby
167-
# This doesn't work - Default mapper isn't pipeline-compatible
168171
RubyEventStore::Mappers::PipelineMapper.new(
169-
RubyEventStore::Mappers::Pipeline.new(
170-
RubyEventStore::Mappers::Default.new(events_class_remapping: upcast_map), # No dump() method
171-
RubyEventStore::Mappers::Transformation::DomainEvent.new,
172-
RubyEventStore::Mappers::Transformation::PreserveTypes.new
173-
)
172+
RubyEventStore::Mappers::Pipeline.new(
173+
RubyEventStore::Mappers::Transformation::Upcast.new(upcast_map),
174+
RubyEventStore::Mappers::Transformation::DomainEvent.new,
175+
RubyEventStore::Mappers::Transformation::SymbolizeMetadataKeys.new,
176+
RubyEventStore::Mappers::Transformation::PreserveTypes.new
174177
)
175-
```
178+
)
176179

177-
We needed `DomainEvent.new`, `SymbolizeMetadataKeys.new`, and `PreserveTypes.new` transformations, but upcast's `Default` mapper isn't designed to work within a transformation pipeline.
178-
179-
### Excessive boilerplate when using lambdas
180-
181-
Lambdas could be used to handle paload transformation, however using upcast with lambdas required significant boilerplate code for each event type:
180+
However, it would require significant boilerplate code for each event type:
182181

183182
```ruby
183+
upcast_map = {
184184
'Ordering::DraftRefundCreated' => lambda { |record|
185-
new_data = symbolize_keys(record.data.dup) # Manual key conversion
186-
new_data = transform_payload(new_data) # Our transformation logic
185+
new_data = record.data.dup
186+
new_data['return_id'] = new_data.delete('refund_id') if new_data['refund_id'] # Repeated logic
187+
new_data['returnable_products'] = new_data.delete('refundable_products') if new_data['refundable_products']
187188
188-
record.class.new( # Manual object creation
189-
event_id: record.event_id, # Boilerplate
189+
record.class.new( # Boilerplate for each lambda
190+
event_id: record.event_id,
190191
event_type: 'Ordering::DraftReturnCreated',
191192
data: new_data,
192-
metadata: symbolize_metadata_keys(record.metadata), # Manual metadata handling
193-
timestamp: record.timestamp, # Manual preservation
194-
valid_at: record.valid_at # Manual preservation
193+
metadata: record.metadata,
194+
timestamp: record.timestamp,
195+
valid_at: record.valid_at
196+
)
197+
},
198+
'Ordering::ItemAddedToRefund' => lambda { |record|
199+
new_data = record.data.dup
200+
new_data['return_id'] = new_data.delete('refund_id') if new_data['refund_id'] # Repeated logic again
201+
202+
record.class.new( # More boilerplate
203+
event_id: record.event_id,
204+
event_type: 'Ordering::ItemAddedToReturn',
205+
data: new_data,
206+
metadata: record.metadata,
207+
timestamp: record.timestamp,
208+
valid_at: record.valid_at
195209
)
196210
}
211+
}
197212
```
198213

199-
This approach would require us to manually implement what DomainEvent.new and PreserveTypes.new handle automatically.
200-
201-
Without the transformation pipeline, we'd lose the automatic benefits of:
202-
- type preservation for timestamps and other complex objects
203-
- metadata key symbolization
204-
- domain event hydration
205-
206-
We'd need to reimplement these features manually in each lambda.
207-
208-
### Code organization and maintainability
214+
Custom Mapper Approach provides:
215+
- single transformation method handles all event types
216+
- clear separation of concerns
217+
- easier unit testing
218+
- better debugging with stack traces pointing to specific methods
209219

210-
Our custom mapper provides better separation of concerns:
211-
- single responsibility: one class handles all transformation logic
212-
- easier testing: clear interface for unit tests
213-
- better debugging: stack traces point to specific transformation methods
214-
- DRY principle: Shared transformation logic across all event types
220+
On the other hand Transformation::Upcast shines for simpler use cases:
221+
- only event class names need changing, no payload transformation
222+
- simple one-to-one event mappings
223+
- minimal transformation logic

0 commit comments

Comments
 (0)