This migration guide covers the major breaking changes introduced in v4.x, which includes a complete architecture overhaul with unified request/response patterns and simplified API design.
- Removed:
CommandWithResult<TResult>interface - Removed:
CommandWithResultHandler<TCommand, TResult>interface - Removed:
Commandinterface (replaced withRequest<TResult>) - Removed:
CommandHandler<TCommand>interface (replaced withRequestHandler<TRequest, TResult>) - Removed:
Query<TResult>interface (replaced withRequest<TResult>) - Removed:
QueryHandler<TQuery, TResult>interface (replaced withRequestHandler<TRequest, TResult>) - Removed:
MediatorBuilderclass - Removed:
CommandProvider,QueryProviderclasses (replaced withRequestProvider)
- Added:
Messagesealed interface as the base for all messages - Added:
Request<TResponse>interface for all request types (commands and queries) - Added:
Request.Unitnested interface for requests that don't return results - Added:
RequestHandler<TRequest, TResult>interface for all request handlers - Added:
RequestHandler.Unit<TRequest>nested interface for unit request handlers - Modified:
Mediatorinterface now has a unifiedsend()method for all requests - Modified:
Mediator.publish()now requires explicitPublishStrategyparameter (with default) - Modified:
PipelineBehaviornow works withMessageinstead of separate types - Modified: All dependency providers now use
RequestHandlerinstead of separate command/query handlers
// Commands (no result)
interface Command {
val type: Class<out Command> get() = this::class.java
}
interface CommandHandler<TCommand : Command> {
suspend fun handle(command: TCommand)
}
// Commands with results
interface CommandWithResult<TResult> {
val type: Class<out CommandWithResult<TResult>> get() = this::class.java
}
interface CommandWithResultHandler<TCommand : CommandWithResult<TResult>, TResult> {
suspend fun handle(command: TCommand): TResult
}
// Queries
interface Query<TResult> {
val type: Class<out Query<TResult>> get() = this::class.java
}
interface QueryHandler<TQuery : Query<TResult>, TResult> {
suspend fun handle(query: TQuery): TResult
}
// Mediator with separate methods
interface Mediator {
suspend fun <TQuery : Query<TResponse>, TResponse> send(query: TQuery): TResponse
suspend fun <TCommand : Command<TResult>, TResult> send(command: TCommand): TResult
suspend fun <T : Notification> publish(notification: T)
suspend fun <T : Notification> publish(notification: T, publishStrategy: PublishStrategy)
}
// MediatorBuilder for configuration
class MediatorBuilder(dependencyProvider: DependencyProvider) {
fun withPublishStrategy(strategy: PublishStrategy): MediatorBuilder
fun build(): Mediator
}// Unified message hierarchy
sealed interface Message
// Unified request interface for commands and queries
interface Request<out TResponse> : Message {
interface Unit : Request<kotlin.Unit>
}
// Unified request handler interface
interface RequestHandler<TRequest : Request<TResult>, TResult> {
suspend fun handle(request: TRequest): TResult
interface Unit<TRequest : Request.Unit> : RequestHandler<TRequest, kotlin.Unit>
}
// Notifications remain the same but now extend Message
interface Notification : Message
// Simplified Mediator interface
interface Mediator {
suspend fun <TRequest : Request<TResponse>, TResponse> send(request: TRequest): TResponse
suspend fun <T : Notification> publish(notification: T, publishStrategy: PublishStrategy = PublishStrategy.DEFAULT)
companion object {
fun build(dependencyProvider: DependencyProvider): Mediator
}
}To ease the migration process, you can temporarily add type aliases to your codebase. This allows you to migrate incrementally without breaking existing code:
// Add these type aliases to ease migration
typealias Command = Request.Unit
typealias CommandWithResult<T> = Request<T>
typealias CommandHandler<TCommand> = RequestHandler.Unit<TCommand>
typealias CommandWithResultHandler<TCommand, TResult> = RequestHandler<TCommand, TResult>
typealias Query<T> = Request<T>
typealias QueryHandler<TQuery, TResult> = RequestHandler<TQuery, TResult>Benefits:
- Allows gradual migration without breaking existing code
- Helps during large codebase migrations
- Can be removed once migration is complete
Usage:
- Add the type aliases to a common file (e.g.,
TypeAliases.kt) - Import them where needed
- Gradually replace usage with the new interfaces
- Remove the type aliases once migration is complete
Before:
class CreateUserCommand : Command {
// command properties
}
class CreateUserCommandHandler : CommandHandler<CreateUserCommand> {
override suspend fun handle(command: CreateUserCommand) {
// handle command
}
}After:
class CreateUserCommand : Request.Unit {
// command properties
}
class CreateUserCommandHandler : RequestHandler.Unit<CreateUserCommand> {
override suspend fun handle(request: CreateUserCommand) {
// handle command
}
}Before:
class GetUserCommand(val userId: String) : CommandWithResult<User> {
// command properties
}
class GetUserCommandHandler : CommandWithResultHandler<GetUserCommand, User> {
override suspend fun handle(command: GetUserCommand): User {
// handle command and return result
return userRepository.findById(command.userId)
}
}After:
class GetUserCommand(val userId: String) : Request<User> {
// command properties
}
class GetUserCommandHandler : RequestHandler<GetUserCommand, User> {
override suspend fun handle(request: GetUserCommand): User {
// handle command and return result
return userRepository.findById(request.userId)
}
}Before:
class GetUserQuery(val userId: String) : Query<User> {
// query properties
}
class GetUserQueryHandler : QueryHandler<GetUserQuery, User> {
override suspend fun handle(query: GetUserQuery): User {
// handle query and return result
return userRepository.findById(query.userId)
}
}After:
class GetUserQuery(val userId: String) : Request<User> {
// query properties
}
class GetUserQueryHandler : RequestHandler<GetUserQuery, User> {
override suspend fun handle(request: GetUserQuery): User {
// handle query and return result
return userRepository.findById(request.userId)
}
}The mediator usage remains mostly the same, but now all requests use the unified send() method:
// Unit commands (no change)
mediator.send(CreateUserCommand())
// Commands with results (no change)
val user = mediator.send(GetUserCommand("123"))
// Queries (no change)
val user = mediator.send(GetUserQuery("123"))
// Notifications now require explicit PublishStrategy (with default)
mediator.publish(UserCreatedNotification(user.id)) // Uses default strategy
mediator.publish(UserCreatedNotification(user.id), PublishStrategy.CONTINUE_ON_EXCEPTION)Before:
val mediator = MediatorBuilder(dependencyProvider)
.withPublishStrategy(ContinueOnExceptionPublishStrategy())
.build()After:
val mediator = Mediator.build(dependencyProvider)
// PublishStrategy is now specified per publish callHandler registration needs to be updated to use the new interfaces:
@Component
class CreateUserCommandHandler : RequestHandler.Unit<CreateUserCommand> {
// implementation
}
@Component
class GetUserCommandHandler : RequestHandler<GetUserCommand, User> {
// implementation
}
@Component
class GetUserQueryHandler : RequestHandler<GetUserQuery, User> {
// implementation
}module {
single { CreateUserCommandHandler() } bind RequestHandler::class
single { GetUserCommandHandler() } bind RequestHandler::class
single { GetUserQueryHandler() } bind RequestHandler::class
}val mediator = HandlerRegistryProvider.createMediator(
handlers = listOf(
CreateUserCommandHandler(),
GetUserCommandHandler(),
GetUserQueryHandler()
)
)Before:
class ParameterizedCommand<T>(val param: T) : Command
class ParameterizedCommandHandler<T> : CommandHandler<ParameterizedCommand<T>> {
override suspend fun handle(command: ParameterizedCommand<T>) {
// handle
}
}After:
class ParameterizedCommand<T>(val param: T) : Request.Unit
class ParameterizedCommandHandler<T> : RequestHandler.Unit<ParameterizedCommand<T>> {
override suspend fun handle(request: ParameterizedCommand<T>) {
// handle
}
}Before:
class ParameterizedCommandWithResult<TParam, TReturn>(
val param: TParam,
val retFn: suspend (TParam) -> TReturn
) : CommandWithResult<TReturn>
class ParameterizedCommandWithResultHandler<TParam, TReturn> :
CommandWithResultHandler<ParameterizedCommandWithResult<TParam, TReturn>, TReturn> {
override suspend fun handle(command: ParameterizedCommandWithResult<TParam, TReturn>): TReturn {
return command.retFn(command.param)
}
}After:
class ParameterizedCommandWithResult<TParam, TReturn>(
val param: TParam,
val retFn: suspend (TParam) -> TReturn
) : Request<TReturn>
class ParameterizedCommandWithResultHandler<TParam, TReturn> :
RequestHandler<ParameterizedCommandWithResult<TParam, TReturn>, TReturn> {
override suspend fun handle(request: ParameterizedCommandWithResult<TParam, TReturn>): TReturn {
return request.retFn(request.param)
}
}Before:
sealed class BaseCommand : Command {
abstract val id: String
}
class SpecificCommand(override val id: String) : BaseCommand()
class BaseCommandHandler : CommandHandler<BaseCommand> {
override suspend fun handle(command: BaseCommand) {
// handle
}
}After:
sealed class BaseCommand : Request.Unit {
abstract val id: String
}
class SpecificCommand(override val id: String) : BaseCommand()
class BaseCommandHandler : RequestHandler.Unit<BaseCommand> {
override suspend fun handle(request: BaseCommand) {
// handle
}
}Before:
class LoggingPipelineBehavior : PipelineBehavior {
override suspend fun <TRequest, TResponse> handle(
request: TRequest,
next: RequestHandlerDelegate<TRequest, TResponse>
): TResponse {
println("Before: $request")
val response = next(request)
println("After: $response")
return response
}
}After:
class LoggingPipelineBehavior : PipelineBehavior {
override suspend fun <TRequest : Message, TResponse> handle(
request: TRequest,
next: RequestHandlerDelegate<TRequest, TResponse>
): TResponse {
println("Before: $request")
val response = next(request)
println("After: $response")
return response
}
}- Unified Architecture: Single
Request<TResult>interface for all request types (commands and queries) - Simplified API: Fewer interfaces to understand - only
Request,RequestHandler, andNotification - Type Safety: Better compile-time type checking with generic result types
- Consistent Patterns: All requests follow the same pattern regardless of type
- Cleaner Dependency Injection: Single
RequestHandlerinterface for all DI frameworks - Flexible Publishing: Explicit control over notification publishing strategies
- Better Testability: Simpler mocking with unified handler interface
- Future-Proof: Extensible architecture that can accommodate new request types
- Replace
Commandimplementations withRequest.Unit - Replace
CommandHandler<TCommand>withRequestHandler.Unit<TCommand> - Replace
CommandWithResult<TResult>withRequest<TResult> - Replace
CommandWithResultHandler<TCommand, TResult>withRequestHandler<TCommand, TResult> - Replace
Query<TResult>implementations withRequest<TResult> - Replace
QueryHandler<TQuery, TResult>withRequestHandler<TQuery, TResult> - Update
MediatorBuilderusage to useMediator.build()directly - Update
Mediator.publish()calls to include explicitPublishStrategy(optional, has default) - Update pipeline behaviors to use
<TRequest : Message, TResponse>constraint - Update dependency injection registrations to use
RequestHandlerinstead of separate handler types - Update import statements to remove references to deleted interfaces
- Update handler method parameters from
command/querytorequest - Test all handlers to ensure they work correctly
- Update any custom extensions or utilities that referenced the old interfaces
-
"Unresolved reference: Command"
- Replace with
Request.Unitfor unit commands
- Replace with
-
"Unresolved reference: CommandWithResult"
- Replace with
Request<TResult>
- Replace with
-
"Unresolved reference: CommandWithResultHandler"
- Replace with
RequestHandler<TCommand, TResult>
- Replace with
-
"Unresolved reference: Query"
- Replace with
Request<TResult>
- Replace with
-
"Unresolved reference: QueryHandler"
- Replace with
RequestHandler<TQuery, TResult>
- Replace with
-
"Unresolved reference: CommandHandler"
- Replace with
RequestHandler.Unit<TCommand>for unit commands - Replace with
RequestHandler<TCommand, TResult>for commands with results
- Replace with
-
"Unresolved reference: MediatorBuilder"
- Replace with
Mediator.build(dependencyProvider)
- Replace with
-
"Type mismatch" errors on unit commands
- Ensure unit commands implement
Request.Unit - Ensure unit handlers implement
RequestHandler.Unit<TCommand>
- Ensure unit commands implement
-
"Wrong number of type arguments" on pipeline behaviors
- Add
Messageconstraint:<TRequest : Message, TResponse>
- Add
-
HandlerNotFoundException
- Ensure handlers are properly registered with dependency injection
- Check that command and handler types match exactly
-
ClassCastException
- Verify generic type parameters are correctly specified
- Ensure handler return types match command result types
If you encounter issues during migration, please:
- Check this migration guide thoroughly
- Review the test examples in the
testFixturesdirectory - Create an issue in the GitHub repository with:
- Your current code
- The error message
- Expected behavior