Tools: CQRS Pattern in Flutter: Commands vs Queries

Tools: CQRS Pattern in Flutter: Commands vs Queries

Source: Dev.to

What is CQRS? ## Base Classes ## Command (Write Operations) ## Query (Read Operations) ## Real Examples from My Auth Feature ## Command: Login ## Query: CheckUserExists ## Query: GetCurrentUser ## Stream Variants ## How to Use in BLoC ## Benefits I've Seen ## Conclusion ## flutter #dart #cleanarchitecture #cqrs When building Flutter applications with Clean Architecture, one pattern that dramatically improves code clarity is Command Query Responsibility Segregation (CQRS). In this article, I'll show you how I implement CQRS in a real Flutter project. CQRS separates read operations (Queries) from write operations (Commands): This separation makes your code: Here are the base classes I use: Login is a Command because it mutates state by creating an authenticated session: CheckUserExists is a Query because it only reads data: For reactive operations, I also have stream-based variants: Example: WatchAuthChanges is a StreamQueryNoParams<User?> that emits whenever the auth state changes. CQRS isn't just for backend systems. In Flutter with Clean Architecture, it brings clarity and maintainability to your use case layer. The key insight: Name your operations by what they DO, and type them by their NATURE (read vs write). This is part of my AI-Ready Flutter Enterprise Starter series. Follow @deveminsahin for more architecture patterns! Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse COMMAND_BLOCK: /// Base class for commands that mutate state abstract class Command<Params, Output> { const Command(); FutureResult<Output> call(Params params); } /// Command without parameters abstract class CommandNoParams<Output> { const CommandNoParams(); FutureResult<Output> call(); } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: /// Base class for commands that mutate state abstract class Command<Params, Output> { const Command(); FutureResult<Output> call(Params params); } /// Command without parameters abstract class CommandNoParams<Output> { const CommandNoParams(); FutureResult<Output> call(); } COMMAND_BLOCK: /// Base class for commands that mutate state abstract class Command<Params, Output> { const Command(); FutureResult<Output> call(Params params); } /// Command without parameters abstract class CommandNoParams<Output> { const CommandNoParams(); FutureResult<Output> call(); } COMMAND_BLOCK: /// Base class for queries that read data abstract class Query<Params, Output> { const Query(); FutureResult<Output> call(Params params); } /// Query without parameters abstract class QueryNoParams<Output> { const QueryNoParams(); FutureResult<Output> call(); } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: /// Base class for queries that read data abstract class Query<Params, Output> { const Query(); FutureResult<Output> call(Params params); } /// Query without parameters abstract class QueryNoParams<Output> { const QueryNoParams(); FutureResult<Output> call(); } COMMAND_BLOCK: /// Base class for queries that read data abstract class Query<Params, Output> { const Query(); FutureResult<Output> call(Params params); } /// Query without parameters abstract class QueryNoParams<Output> { const QueryNoParams(); FutureResult<Output> call(); } COMMAND_BLOCK: @injectable class Login extends Command<AuthCredentials, User> { const Login(this._repository, this._eventDispatcher); final IAuthRepository _repository; final IEventDispatcher _eventDispatcher; @override FutureResult<User> call(AuthCredentials credentials) async { // Mutate state: authenticate user final result = await _repository.login(credentials); // Dispatch domain event on success return result.map((user) { _eventDispatcher.dispatch(UserLoggedIn(user)); return user; }); } } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: @injectable class Login extends Command<AuthCredentials, User> { const Login(this._repository, this._eventDispatcher); final IAuthRepository _repository; final IEventDispatcher _eventDispatcher; @override FutureResult<User> call(AuthCredentials credentials) async { // Mutate state: authenticate user final result = await _repository.login(credentials); // Dispatch domain event on success return result.map((user) { _eventDispatcher.dispatch(UserLoggedIn(user)); return user; }); } } COMMAND_BLOCK: @injectable class Login extends Command<AuthCredentials, User> { const Login(this._repository, this._eventDispatcher); final IAuthRepository _repository; final IEventDispatcher _eventDispatcher; @override FutureResult<User> call(AuthCredentials credentials) async { // Mutate state: authenticate user final result = await _repository.login(credentials); // Dispatch domain event on success return result.map((user) { _eventDispatcher.dispatch(UserLoggedIn(user)); return user; }); } } COMMAND_BLOCK: @injectable class CheckUserExists extends Query<EmailAddress, bool> { const CheckUserExists(this._repository); final IAuthRepository _repository; @override FutureResult<bool> call(EmailAddress email) async { // Read-only: check if user exists return _repository.checkUserExists(email); } } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: @injectable class CheckUserExists extends Query<EmailAddress, bool> { const CheckUserExists(this._repository); final IAuthRepository _repository; @override FutureResult<bool> call(EmailAddress email) async { // Read-only: check if user exists return _repository.checkUserExists(email); } } COMMAND_BLOCK: @injectable class CheckUserExists extends Query<EmailAddress, bool> { const CheckUserExists(this._repository); final IAuthRepository _repository; @override FutureResult<bool> call(EmailAddress email) async { // Read-only: check if user exists return _repository.checkUserExists(email); } } COMMAND_BLOCK: @injectable class GetCurrentUser extends QueryNoParams<User?> { const GetCurrentUser(this._repository, this._eventDispatcher); final IAuthRepository _repository; final IEventDispatcher _eventDispatcher; @override FutureResult<User?> call() async { // Read-only: get current user from cache/backend final result = await _repository.getCurrentUser(); return result.map((user) { if (user != null) { _eventDispatcher.dispatch(UserSessionRestored(user)); } return user; }); } } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: @injectable class GetCurrentUser extends QueryNoParams<User?> { const GetCurrentUser(this._repository, this._eventDispatcher); final IAuthRepository _repository; final IEventDispatcher _eventDispatcher; @override FutureResult<User?> call() async { // Read-only: get current user from cache/backend final result = await _repository.getCurrentUser(); return result.map((user) { if (user != null) { _eventDispatcher.dispatch(UserSessionRestored(user)); } return user; }); } } COMMAND_BLOCK: @injectable class GetCurrentUser extends QueryNoParams<User?> { const GetCurrentUser(this._repository, this._eventDispatcher); final IAuthRepository _repository; final IEventDispatcher _eventDispatcher; @override FutureResult<User?> call() async { // Read-only: get current user from cache/backend final result = await _repository.getCurrentUser(); return result.map((user) { if (user != null) { _eventDispatcher.dispatch(UserSessionRestored(user)); } return user; }); } } COMMAND_BLOCK: // Stream query - watch data changes abstract class StreamQuery<Params, Output> { StreamResult<Output> call(Params params); } // Stream command - reactive write operations abstract class StreamCommand<Params, Output> { StreamResult<Output> call(Params params); } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: // Stream query - watch data changes abstract class StreamQuery<Params, Output> { StreamResult<Output> call(Params params); } // Stream command - reactive write operations abstract class StreamCommand<Params, Output> { StreamResult<Output> call(Params params); } COMMAND_BLOCK: // Stream query - watch data changes abstract class StreamQuery<Params, Output> { StreamResult<Output> call(Params params); } // Stream command - reactive write operations abstract class StreamCommand<Params, Output> { StreamResult<Output> call(Params params); } COMMAND_BLOCK: class AuthBloc extends Bloc<AuthEvent, AuthState> { AuthBloc(this._login, this._checkUserExists, this._getCurrentUser); final Login _login; final CheckUserExists _checkUserExists; final GetCurrentUser _getCurrentUser; Future<void> _onSubmitCredentials(event, emit) async { // Use Command for login final result = await _login(AuthCredentials( email: event.email, password: event.password, )); result.fold( (failure) => emit(AuthState.error(failure)), (user) => emit(AuthState.authenticated(user)), ); } Future<void> _onEmailSubmitted(event, emit) async { // Use Query for checking user final result = await _checkUserExists(event.email); result.fold( (failure) => emit(AuthState.error(failure)), (exists) => exists ? emit(AuthState.loginRequired(event.email)) : emit(AuthState.registrationRequired(event.email)), ); } } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: class AuthBloc extends Bloc<AuthEvent, AuthState> { AuthBloc(this._login, this._checkUserExists, this._getCurrentUser); final Login _login; final CheckUserExists _checkUserExists; final GetCurrentUser _getCurrentUser; Future<void> _onSubmitCredentials(event, emit) async { // Use Command for login final result = await _login(AuthCredentials( email: event.email, password: event.password, )); result.fold( (failure) => emit(AuthState.error(failure)), (user) => emit(AuthState.authenticated(user)), ); } Future<void> _onEmailSubmitted(event, emit) async { // Use Query for checking user final result = await _checkUserExists(event.email); result.fold( (failure) => emit(AuthState.error(failure)), (exists) => exists ? emit(AuthState.loginRequired(event.email)) : emit(AuthState.registrationRequired(event.email)), ); } } COMMAND_BLOCK: class AuthBloc extends Bloc<AuthEvent, AuthState> { AuthBloc(this._login, this._checkUserExists, this._getCurrentUser); final Login _login; final CheckUserExists _checkUserExists; final GetCurrentUser _getCurrentUser; Future<void> _onSubmitCredentials(event, emit) async { // Use Command for login final result = await _login(AuthCredentials( email: event.email, password: event.password, )); result.fold( (failure) => emit(AuthState.error(failure)), (user) => emit(AuthState.authenticated(user)), ); } Future<void> _onEmailSubmitted(event, emit) async { // Use Query for checking user final result = await _checkUserExists(event.email); result.fold( (failure) => emit(AuthState.error(failure)), (exists) => exists ? emit(AuthState.loginRequired(event.email)) : emit(AuthState.registrationRequired(event.email)), ); } } - ✅ Easier to test (queries are always idempotent) - ✅ Clearer intent (naming says what it does) - ✅ Simpler to maintain (each class has one responsibility) - Clear Intent: Just by reading Login extends Command<...>, you know it mutates state - Testability: Queries are pure - same input = same output - SOLID Compliance: Each use case has a single responsibility - Discoverability: Team members can easily find all write vs read operations