Migration Guide
This guide helps you migrate from traditional architecture to the Wow framework, as well as upgrade between different versions.
Version Upgrade Guide
Upgrade Steps
- Backup Data: Backup event store and snapshot data before upgrading
- Read Changelog: Check Release Notes
- Update Dependency Version: Modify build.gradle.kts or pom.xml
- Run Tests: Ensure all tests pass
- Gradual Rollout: Gradually upgrade production environment
Dependency Version Update
// Update wow version
implementation("me.ahoo.wow:wow-spring-boot-starter:new-version")<dependency>
<groupId>me.ahoo.wow</groupId>
<artifactId>wow-spring-boot-starter</artifactId>
<version>new-version</version>
</dependency>Breaking Changes Check
Before upgrading, check the following:
- API Changes: Check for interface signature changes
- Configuration Changes: Check for configuration property changes
- Metadata Changes: Regenerate metadata files
Mongo Ownership Guard and Snapshot Checkpoints
This upgrade keeps aggregate-name-only Mongo collection names, but adds a durable wow_database_metadata ownership marker. The supported deployment layout is one bounded context per MongoDB database.
Before rollout:
- Inspect every configured event-stream, snapshot, and prepare database. Check all
*_event_stream,*_snapshot,*_snapshot_checkpoint, andprepare_*collections. - Confirm that each database belongs to only one
wow.context-name; a mixed database must be split before upgrade. - Upgrade the database's real owner first. The first upgraded instance scans legacy aggregate collections before atomically claiming the marker. Legacy
prepare_*records contain no context metadata, so a prepare-only database is claimed by the first upgraded context and must be audited before rollout. - Audit existing managed indexes. Missing indexes are created, but incompatible key order, uniqueness, TTL, partial-filter, collation, sparse, or hidden options block startup and require a controlled migration.
Do not edit the marker to bypass a context mismatch. Move or remove the old data, then remove the marker only when the database is intentionally reassigned.
Historical snapshot checkpoints are disabled by default:
wow:
eventsourcing:
snapshot:
checkpoint:
enabled: false
version-interval: 100Enabling the feature creates <aggregate>_snapshot_checkpoint sidecars and stores only newly produced matching versions; it does not backfill history. Roll back by disabling the feature, and retain the sidecars until validation and retention decisions are complete.
Migrating from Traditional Architecture
Migration Strategy
Gradual Migration
We recommend a gradual migration strategy, progressively migrating functional modules to event sourcing architecture:
Migration Steps
- Identify Bounded Contexts: Determine business modules to migrate
- Design Domain Model: Define aggregate roots, commands, and events
- Implement Dual Writing: Write to both old and new systems
- Verify Consistency: Ensure data consistency
- Switch Read/Write: Gradually switch to new system
Data Migration
Historical Data Import
For scenarios requiring historical data preservation, it is recommended to define migration commands:
// 1. Define Migration Command
@CreateAggregate
data class MigrateOrder(
val orderId: String,
val customerId: String,
val items: List<OrderItem>,
val createdAt: Long
)
// 2. Handle Migration Command in Aggregate
@AggregateRoot
class Order(private val state: OrderState) {
@OnCommand
fun onMigrate(command: MigrateOrder): OrderCreated {
return OrderCreated(
orderId = command.orderId,
customerId = command.customerId,
items = command.items,
createdAt = command.createdAt
)
}
}
// 3. Send Migration Command
fun migrateHistoricalData(legacyOrders: List<LegacyOrder>) {
legacyOrders.forEach { order ->
val command = MigrateOrder(
orderId = order.id,
customerId = order.customerId,
items = order.items.map { /* convert */ },
createdAt = order.createdAt
)
commandGateway.send(command).block()
}
}Code Migration
From CRUD to Command Pattern
Traditional CRUD Code:
// Traditional service
@Service
class OrderService(private val orderRepository: OrderRepository) {
fun createOrder(request: CreateOrderRequest): Order {
val order = Order(
id = UUID.randomUUID().toString(),
customerId = request.customerId,
items = request.items,
status = OrderStatus.CREATED
)
return orderRepository.save(order)
}
fun updateOrderStatus(orderId: String, status: OrderStatus) {
val order = orderRepository.findById(orderId)
order.status = status
orderRepository.save(order)
}
}Migrated Wow Code:
// Command definitions
@CreateAggregate
data class CreateOrder(
val customerId: String,
val items: List<OrderItem>
)
@CommandRoute
data class UpdateOrderStatus(
@AggregateId val id: String,
val status: OrderStatus
)
// Aggregate root
@AggregateRoot
class Order(private val state: OrderState) {
@OnCommand
fun onCreate(command: CreateOrder): OrderCreated {
return OrderCreated(
customerId = command.customerId,
items = command.items
)
}
@OnCommand
fun onUpdateStatus(command: UpdateOrderStatus): OrderStatusUpdated {
return OrderStatusUpdated(command.status)
}
}
// State aggregate root
class OrderState : Identifier {
lateinit var id: String
lateinit var customerId: String
var items: List<OrderItem> = emptyList()
var status: OrderStatus = OrderStatus.CREATED
fun onSourcing(event: OrderCreated) {
this.customerId = event.customerId
this.items = event.items
}
fun onSourcing(event: OrderStatusUpdated) {
this.status = event.status
}
}From Direct Queries to Query Snapshots
Traditional Query Code:
@Repository
interface OrderRepository : JpaRepository<Order, String> {
fun findByCustomerId(customerId: String): List<Order>
fun findByStatus(status: OrderStatus): List<Order>
}Migrated Query Code:
Refer to Query Service
class OrderService(
private val queryService: SnapshotQueryService<OrderState>
) {
fun getById(id: String): Mono<OrderState> {
return singleQuery {
condition {
id(id)
}
}.query(queryService).toState().throwNotFoundIfEmpty()
}
}Compatibility Notes
Data Format Compatibility
The Wow framework uses JSON serialization for events and snapshot data, ensuring good forward compatibility:
- Adding Fields: New fields will be ignored (backward compatible)
- Removing Fields: Uses default values (needs handling)
- Changing Field Types: Requires event upgrader
Event Upgrades
Use the revision attribute of the @Event annotation for event version control:
@Event(revision = "1.0")
data class OrderCreatedV1(
val orderId: String,
val items: List<OrderItem>
)
@Event(revision = "2.0")
data class OrderCreated(
val orderId: String,
val items: List<OrderItem>,
val customerId: String // New field
)Message Format Compatibility
Ensure message format compatibility:
- Adding Fields: Safe, uses default values
- Removing Fields: Need to ensure consumers can handle
- Renaming Fields: Not compatible, requires version control
Known Issues
Version-Specific Issues
Please check GitHub Issues for the latest known issues list.
Common Migration Issues
- Event Replay Order: Ensure events are appended in version order
- Timestamp Handling: Preserve original timestamps
- ID Generation: Maintain consistent ID format
Migration Checklist
- [ ] Backup existing data
- [ ] Update dependency version
- [ ] Check breaking changes
- [ ] Update configuration files
- [ ] Regenerate metadata
- [ ] Run unit tests
- [ ] Run integration tests
- [ ] Gradual rollout verification
- [ ] Full rollout
- [ ] Monitoring verification
Rollback Plan
If migration fails, follow these rollback steps:
- Stop new service
- Restore old service
- Verify data consistency
- Analyze failure cause
- Fix issues and retry