Using BLoC with CQRS in Flutter
Oct 7, 2025



Summary
Summary
Summary
Summary
This tutorial shows how to combine BLoC and CQRS in Flutter mobile development: design immutable commands and tailored queries, place domain logic in command handlers, expose read-model streams via query services, and let BLoC orchestrate dispatch and subscription. The approach improves testability, offline support, and UI performance while keeping responsibilities clear.
This tutorial shows how to combine BLoC and CQRS in Flutter mobile development: design immutable commands and tailored queries, place domain logic in command handlers, expose read-model streams via query services, and let BLoC orchestrate dispatch and subscription. The approach improves testability, offline support, and UI performance while keeping responsibilities clear.
This tutorial shows how to combine BLoC and CQRS in Flutter mobile development: design immutable commands and tailored queries, place domain logic in command handlers, expose read-model streams via query services, and let BLoC orchestrate dispatch and subscription. The approach improves testability, offline support, and UI performance while keeping responsibilities clear.
This tutorial shows how to combine BLoC and CQRS in Flutter mobile development: design immutable commands and tailored queries, place domain logic in command handlers, expose read-model streams via query services, and let BLoC orchestrate dispatch and subscription. The approach improves testability, offline support, and UI performance while keeping responsibilities clear.
Key insights:
Key insights:
Key insights:
Key insights:
Principles of CQRS and BLoC: Separate write and read responsibilities; use BLoC for orchestration and state emission.
Designing Commands and Queries: Commands are immutable intents; queries return optimized read models for the UI.
Implementing BLoC with CQRS: BLoC dispatches commands to handlers and subscribes to query streams for UI updates.
Testing and Best Practices: Test handlers and queries independently; mock services in BLoC tests and persist read models for offline UX.
Performance Considerations: Use cached, memoized read models and local persistence to keep UIs responsive.
Introduction
In mobile development with Flutter, organizing state and side effects becomes crucial as apps grow. CQRS (Command Query Responsibility Segregation) separates writes (commands) from reads (queries). BLoC (Business Logic Component) provides a predictable, testable pattern for state management. Combining BLoC with CQRS gives you clear flows for intent, validation, side effects, and read-model composition. This article explains practical patterns, minimal code examples, and testing tips to apply CQRS inside a Flutter app using BLoC.
Principles of CQRS and BLoC
CQRS prescribes two orthogonal responsibilities: Commands mutate state and return success/failure, while Queries return a projection of state without side effects. BLoC is already aligned to handle event-driven intents and produce state streams. Use BLoC to accept command events and to orchestrate query subscriptions for read models.
Key principles to keep in mind:
Keep write model logic encapsulated: validation, domain rules, and side effects belong to command handlers.
Keep read models optimized for UI consumption: queries can be precomputed, cached, or composed from local storage + network.
Keep a thin BLoC boundary: BLoC should orchestrate handlers and emit UI-ready states rather than being a dumping ground for domain logic.
This separation improves testability and allows independent scaling of read and write paths in mobile apps where offline sync, background jobs, and UI performance matter.
Designing Commands and Queries
Design commands as immutable objects representing an intent. Commands should be simple DTOs validated by command handlers or usecases. Queries should declare the projection needed by the UI and may accept filters or pagination.
Example shapes:
Commands: CreateTodoCommand, UpdateProfileCommand, SubmitOrderCommand
Queries: TodosListQuery, ProfileQuery, OrderDetailQuery
Command handlers should return a result type that clearly indicates success or domain errors. Use a Result/Either type or a sealed union so BLoC can map outcomes to states.
Keep read models tailored to screens. For mobile performance, consider local persistence for queries (sqflite, Hive) and stream updates to the BLoC via repositories.
Implementing BLoC with CQRS
Structure your project around layers: UI -> BLoC -> UseCases/CommandHandlers & QueryServices -> Repositories -> DataSources. The BLoC receives UI events that translate to Commands or Query subscriptions. Command handlers perform domain logic and push events to persistence or an event bus; Query services expose streams or futures for read models.
Example showing command models and a query service:
class CreateTodoCommand { final String title; CreateTodoCommand(this.title); }
class TodosListQuery { final bool showCompleted; TodosListQuery(this.showCompleted); }
abstract class TodoQueryService { Stream<List<TodoView>> stream(TodosListQuery q); }A BLoC can handle both command dispatching and query subscriptions. Keep command handling asynchronous and map results to states. Here's a compact BLoC sketch:
class TodoBloc extends Bloc<TodoEvent, TodoState> {
final CommandDispatcher dispatcher;
final TodoQueryService queries;
TodoBloc(this.dispatcher, this.queries): super(TodoInitial()) {
on<CreateTodoEvent>((e, emit) async {
emit(TodoLoading());
final res = await dispatcher.dispatch(CreateTodoCommand(e.title));
emit(res.isSuccess ? TodoSuccess() : TodoFailure(res.error));
});
on<SubscribeTodosEvent>((e, emit) async {
await emit.forEach(queries.stream(TodosListQuery(e.showCompleted)),
onData: (list) => TodosLoaded(list));
});
}
}Notes:
CommandDispatcher is a thin abstraction that routes to command handlers or use cases and returns a Result.
QueryService returns streams for long-lived subscriptions; BLoC uses emit.forEach to forward updates.
Keep side effects (analytics, navigation) outside domain handlers; let BLoC react to success states and trigger UI-level effects through other mechanisms.
Testing and Best Practices
Testing each layer independently is where CQRS shines:
Unit test command handlers for domain rules and side effects, mocking repositories and external adapters.
Unit test query services for projection correctness and caching behavior.
Test BLoCs by mocking CommandDispatcher and QueryService; verify state transitions and stream handling.
Best practices for mobile development:
Use small commands and explicit result types to avoid ambiguous failure handling.
Persist read models locally to enable instant UI updates and offline support; sync writes in the background via a queue.
Consider optimistic UI updates: emit a provisional state in BLoC before the command completes, then reconcile on result.
Keep BLoC responsibilities limited to orchestration and state emission; heavyweight domain logic belongs in handlers/use cases.
Performance tips: avoid re-computing large projections in the UI layer. Let query services produce minimal, memoized view models.
Vibe Studio

Vibe Studio, powered by Steve’s advanced AI agents, is a revolutionary no-code, conversational platform that empowers users to quickly and efficiently create full-stack Flutter applications integrated seamlessly with Firebase backend services. Ideal for solo founders, startups, and agile engineering teams, Vibe Studio allows users to visually manage and deploy Flutter apps, greatly accelerating the development process. The intuitive conversational interface simplifies complex development tasks, making app creation accessible even for non-coders.
Conclusion
Combining BLoC with CQRS in Flutter yields a maintainable, testable architecture for mobile development. Use BLoC to orchestrate command dispatch and query subscriptions, keep domain rules in command handlers, and design read models for UI performance and offline support. The clear separation reduces coupling, simplifies testing, and makes complex flows (sync, conflict resolution, optimistic updates) tractable. Start small: introduce command/query abstractions for a single feature, and iterate as the benefits become clear.
Introduction
In mobile development with Flutter, organizing state and side effects becomes crucial as apps grow. CQRS (Command Query Responsibility Segregation) separates writes (commands) from reads (queries). BLoC (Business Logic Component) provides a predictable, testable pattern for state management. Combining BLoC with CQRS gives you clear flows for intent, validation, side effects, and read-model composition. This article explains practical patterns, minimal code examples, and testing tips to apply CQRS inside a Flutter app using BLoC.
Principles of CQRS and BLoC
CQRS prescribes two orthogonal responsibilities: Commands mutate state and return success/failure, while Queries return a projection of state without side effects. BLoC is already aligned to handle event-driven intents and produce state streams. Use BLoC to accept command events and to orchestrate query subscriptions for read models.
Key principles to keep in mind:
Keep write model logic encapsulated: validation, domain rules, and side effects belong to command handlers.
Keep read models optimized for UI consumption: queries can be precomputed, cached, or composed from local storage + network.
Keep a thin BLoC boundary: BLoC should orchestrate handlers and emit UI-ready states rather than being a dumping ground for domain logic.
This separation improves testability and allows independent scaling of read and write paths in mobile apps where offline sync, background jobs, and UI performance matter.
Designing Commands and Queries
Design commands as immutable objects representing an intent. Commands should be simple DTOs validated by command handlers or usecases. Queries should declare the projection needed by the UI and may accept filters or pagination.
Example shapes:
Commands: CreateTodoCommand, UpdateProfileCommand, SubmitOrderCommand
Queries: TodosListQuery, ProfileQuery, OrderDetailQuery
Command handlers should return a result type that clearly indicates success or domain errors. Use a Result/Either type or a sealed union so BLoC can map outcomes to states.
Keep read models tailored to screens. For mobile performance, consider local persistence for queries (sqflite, Hive) and stream updates to the BLoC via repositories.
Implementing BLoC with CQRS
Structure your project around layers: UI -> BLoC -> UseCases/CommandHandlers & QueryServices -> Repositories -> DataSources. The BLoC receives UI events that translate to Commands or Query subscriptions. Command handlers perform domain logic and push events to persistence or an event bus; Query services expose streams or futures for read models.
Example showing command models and a query service:
class CreateTodoCommand { final String title; CreateTodoCommand(this.title); }
class TodosListQuery { final bool showCompleted; TodosListQuery(this.showCompleted); }
abstract class TodoQueryService { Stream<List<TodoView>> stream(TodosListQuery q); }A BLoC can handle both command dispatching and query subscriptions. Keep command handling asynchronous and map results to states. Here's a compact BLoC sketch:
class TodoBloc extends Bloc<TodoEvent, TodoState> {
final CommandDispatcher dispatcher;
final TodoQueryService queries;
TodoBloc(this.dispatcher, this.queries): super(TodoInitial()) {
on<CreateTodoEvent>((e, emit) async {
emit(TodoLoading());
final res = await dispatcher.dispatch(CreateTodoCommand(e.title));
emit(res.isSuccess ? TodoSuccess() : TodoFailure(res.error));
});
on<SubscribeTodosEvent>((e, emit) async {
await emit.forEach(queries.stream(TodosListQuery(e.showCompleted)),
onData: (list) => TodosLoaded(list));
});
}
}Notes:
CommandDispatcher is a thin abstraction that routes to command handlers or use cases and returns a Result.
QueryService returns streams for long-lived subscriptions; BLoC uses emit.forEach to forward updates.
Keep side effects (analytics, navigation) outside domain handlers; let BLoC react to success states and trigger UI-level effects through other mechanisms.
Testing and Best Practices
Testing each layer independently is where CQRS shines:
Unit test command handlers for domain rules and side effects, mocking repositories and external adapters.
Unit test query services for projection correctness and caching behavior.
Test BLoCs by mocking CommandDispatcher and QueryService; verify state transitions and stream handling.
Best practices for mobile development:
Use small commands and explicit result types to avoid ambiguous failure handling.
Persist read models locally to enable instant UI updates and offline support; sync writes in the background via a queue.
Consider optimistic UI updates: emit a provisional state in BLoC before the command completes, then reconcile on result.
Keep BLoC responsibilities limited to orchestration and state emission; heavyweight domain logic belongs in handlers/use cases.
Performance tips: avoid re-computing large projections in the UI layer. Let query services produce minimal, memoized view models.
Vibe Studio

Vibe Studio, powered by Steve’s advanced AI agents, is a revolutionary no-code, conversational platform that empowers users to quickly and efficiently create full-stack Flutter applications integrated seamlessly with Firebase backend services. Ideal for solo founders, startups, and agile engineering teams, Vibe Studio allows users to visually manage and deploy Flutter apps, greatly accelerating the development process. The intuitive conversational interface simplifies complex development tasks, making app creation accessible even for non-coders.
Conclusion
Combining BLoC with CQRS in Flutter yields a maintainable, testable architecture for mobile development. Use BLoC to orchestrate command dispatch and query subscriptions, keep domain rules in command handlers, and design read models for UI performance and offline support. The clear separation reduces coupling, simplifies testing, and makes complex flows (sync, conflict resolution, optimistic updates) tractable. Start small: introduce command/query abstractions for a single feature, and iterate as the benefits become clear.
Build Flutter Apps Faster with Vibe Studio
Build Flutter Apps Faster with Vibe Studio
Build Flutter Apps Faster with Vibe Studio
Build Flutter Apps Faster with Vibe Studio
Vibe Studio is your AI-powered Flutter development companion. Skip boilerplate, build in real-time, and deploy without hassle. Start creating apps at lightning speed with zero setup.
Vibe Studio is your AI-powered Flutter development companion. Skip boilerplate, build in real-time, and deploy without hassle. Start creating apps at lightning speed with zero setup.
Vibe Studio is your AI-powered Flutter development companion. Skip boilerplate, build in real-time, and deploy without hassle. Start creating apps at lightning speed with zero setup.
Vibe Studio is your AI-powered Flutter development companion. Skip boilerplate, build in real-time, and deploy without hassle. Start creating apps at lightning speed with zero setup.






















