cqrs-pattern

naporin0624's avatarfrom naporin0624

Implement CQRS (Command Query Responsibility Segregation) pattern for Electron services. Use when creating service methods, separating read/write operations, or implementing reactive data streams with Observable for queries and ResultAsync for commands.

⭐0stars🔀0forks📁View on GitHub🕐Updated Dec 29, 2025

When & Why to Use This Skill

This Claude skill facilitates the implementation of the Command Query Responsibility Segregation (CQRS) pattern specifically optimized for Electron services. By leveraging RxJS Observables for reactive data queries and neverthrow's ResultAsync for robust command execution, it enables developers to build scalable, maintainable, and type-safe desktop applications with a clear separation between read and write operations.

Use Cases

  • Architecting reactive Electron applications where the UI needs to automatically update in real-time as the underlying data state changes.
  • Implementing standardized error handling and functional programming patterns in service layers to replace traditional try-catch blocks with ResultAsync.
  • Refactoring complex Electron service classes to improve maintainability by decoupling data retrieval logic from state-modifying side effects.
  • Building robust data synchronization bridges between the Electron main process and renderer using BehaviorSubject and reactive streams.
namecqrs-pattern
descriptionImplement CQRS (Command Query Responsibility Segregation) pattern for Electron services. Use when creating service methods, separating read/write operations, or implementing reactive data streams with Observable for queries and ResultAsync for commands.
allowed-toolsRead, Write, Edit, Grep, Glob

CQRS Pattern for Electron Services

Overview

This skill documents the CQRS (Command Query Responsibility Segregation) pattern used in Electron applications with RxJS and neverthrow.

Core Principles

Aspect Query (Read) Command (Write)
Purpose Read data without side effects Modify state
Return Type Observable<T> ResultAsync<void, Error>
Side Effects None Database writes, notifications
Idempotency Always idempotent May not be idempotent

Quick Start

1. Define Service Interface

// src/shared/services/user.service.d.ts
import type { Observable } from 'rxjs';
import type { ResultAsync } from 'neverthrow';
import type { ApplicationError } from '@shared/typing';

interface UserService {
  // Queries - Return Observable
  list(): Observable<User[]>;
  get(id: string): Observable<User | undefined>;

  // Commands - Return ResultAsync
  create(data: CreateUserData): ResultAsync<void, ApplicationError>;
  update(data: UpdateUserData): ResultAsync<void, ApplicationError>;
  delete(id: string): ResultAsync<void, ApplicationError>;
}

2. Implement Service with BehaviorSubject Bridge

// src/main/services/user.service.ts
import { BehaviorSubject, Observable, concat, from } from 'rxjs';
import { distinctUntilChanged, mergeMap } from 'rxjs/operators';
import { ResultAsync, okAsync, errAsync } from 'neverthrow';

export class UserServiceImpl implements UserService {
  #notify = new BehaviorSubject(Date.now());

  constructor(private readonly db: Database) {}

  // QUERY: Returns Observable
  list(): Observable<User[]> {
    return concat(
      from(this.#getUsers()),                    // Initial fetch
      this.#notify.pipe(
        distinctUntilChanged(),
        mergeMap(() => this.#getUsers())         // Re-fetch on changes
      )
    );
  }

  // COMMAND: Returns ResultAsync
  create(data: CreateUserData): ResultAsync<void, ApplicationError> {
    return this.#insertUser(data)
      .andThen(() => {
        this.#notify.next(Date.now());           // Notify subscribers
        return okAsync(void 0);
      });
  }

  // Private: Actual database operation
  async #getUsers(): Promise<User[]> {
    return this.db.query.users.findMany();
  }

  #insertUser(data: CreateUserData): ResultAsync<void, ApplicationError> {
    return ResultAsync.fromPromise(
      this.db.insert(users).values(data),
      (e) => new ApplicationError('Failed to create user', e)
    ).map(() => void 0);
  }
}

Architecture Diagram

┌─────────────────────────────────────────────────────────────┐
│                   SERVICE LAYER (CQRS)                       │
├─────────────────────────────────────────────────────────────┤
│                                                               │
│   #notify = new BehaviorSubject(Date.now())                  │
│              │                                                │
│   ┌──────────┴──────────┐                                    │
│   │                     │                                    │
│   ▼                     ▼                                    │
│ QUERY                 COMMAND                                │
│ Observable<T>         ResultAsync<void>                      │
│   │                     │                                    │
│   │ concat(             │ #performOperation()               │
│   │   from(#getData()), │   .andThen(() => {                │
│   │   #notify.pipe(     │     #notify.next(Date.now());     │
│   │     mergeMap(...)   │     return okAsync(void 0);       │
│   │   )                 │   })                               │
│   │ )                   │                                    │
│   │                     │                                    │
│   └──────────┬──────────┘                                    │
│              │                                                │
│              ▼                                                │
│        DATABASE                                              │
└─────────────────────────────────────────────────────────────┘

When to Use

Use QUERY (Observable) for:

  • Listing entities: list(), histories()
  • Getting single entity: get(id), active()
  • Streaming updates: notify() (returns Subject/Observable)
  • Any read operation that benefits from reactive updates

Use COMMAND (ResultAsync) for:

  • Creating entities: create(data)
  • Updating entities: update(data)
  • Deleting entities: delete(id)
  • Actions with side effects: invite(), send()

Additional Resources