How to Implement CQRS Pattern for Command and Query Separation in Java

The Command Query Responsibility Segregation (CQRS) pattern splits application logic into two distinct layers: commands that mutate state within transactions, and queries that execute read-only operations returning lightweight DTOs.

The java-design-patterns repository by iluwatar demonstrates how to implement CQRS pattern for command and query separation through a practical library management example. Located in the command-query-responsibility-segregation module, this implementation uses Hibernate to illustrate clear architectural boundaries between write operations (commands) and read operations (queries).

Core Concepts of CQRS Architecture

CQRS fundamentally separates commands (operations that change application state) from queries (operations that retrieve data). In the java-design-patterns implementation, this separation manifests as two independent service interfaces: CommandService for persistent writes and QueryService for optimized reads.

The command side opens Hibernate sessions with explicit transactions to ensure data integrity, while the query side executes HQL or native SQL without transactional overhead, projecting results directly into immutable DTOs. This architectural split enables independent scaling, performance optimization, and simplified maintenance of complex domain models.

Command Side Implementation (Write Model)

The write model handles all state mutations through the CommandService interface defined in command-query-responsibility-segregation/src/main/java/com/iluwatar/cqrs/commandes/CommandService.java.

Command Service Contract

The interface declares methods for creating and updating domain entities:

public interface CommandService {
  void authorCreated(String username, String name, String email);
  void bookAddedToAuthor(String title, double price, String username);
  void authorNameUpdated(String username, String name);
  // Additional mutating operations...
}

Command Service Implementation

The CommandServiceImpl class in command-query-responsibility-segregation/src/main/java/com/iluwatar/cqrs/commandes/CommandServiceImpl.java wraps each operation in a Hibernate transaction:

public class CommandServiceImpl implements CommandService {
  private final SessionFactory sessionFactory = HibernateUtil.getSessionFactory();

  @Override
  public void authorCreated(String username, String name, String email) {
    var author = new Author(username, name, email);
    try (var session = sessionFactory.openSession()) {
      session.beginTransaction();
      session.save(author);                     // Persist new entity
      session.getTransaction().commit();        // Commit write transaction
    }
  }
  // Additional methods follow: open session → begin → update → commit
}

Each command method follows the unit-of-work pattern: opening a session, beginning a transaction, persisting or updating entities, and committing changes. This ensures atomicity for all write operations.

Query Side Implementation (Read Model)

The read model bypasses domain entities entirely, returning purpose-built DTOs from com.iluwatar.cqrs.dto. The QueryService interface in command-query-responsibility-segregation/src/main/java/com/iluwatar/cqrs/queries/QueryService.java defines read-only operations:

public interface QueryService {
  Author getAuthorByUsername(String username);
  Book getBook(String title);
  List<Book> getAuthorBooks(String username);
  BigInteger getAuthorBooksCount(String username);
  BigInteger getAuthorsCount();
}

Query Service Implementation

The QueryServiceImpl class in command-query-responsibility-segregation/src/main/java/com/iluwatar/cqrs/queries/QueryServiceImpl.java executes queries without transactional boundaries:

public class QueryServiceImpl implements QueryService {
  private final SessionFactory sessionFactory = HibernateUtil.getSessionFactory();

  @Override
  public Author getAuthorByUsername(String username) {
    try (var session = sessionFactory.openSession()) {
      Query<Author> q = session.createQuery(
        "select new com.iluwatar.cqrs.dto.Author(a.name, a.email, a.username) " +
        "from com.iluwatar.cqrs.domain.model.Author a where a.username=:username");
      q.setParameter(AppConstants.USER_NAME, username);
      return q.uniqueResult();                  // Read-only, no transaction needed
    }
  }
  // Additional methods use native queries for counts or list projections
}

Notice the use of constructor expressions in HQL (select new com.iluwatar.cqrs.dto.Author(...)) to project domain data directly into DTOs, eliminating the need for entity hydration and reducing memory overhead.

Wiring Commands and Queries in Application Code

The App class in command-query-responsibility-segregation/src/main/java/com/iluwatar/cqrs/app/App.java demonstrates how to coordinate both services:

public static void main(String[] args) {
  var commands = new CommandServiceImpl();
  commands.authorCreated(AppConstants.E_EVANS, "Eric Evans", "evans@email.com");
  // Additional write operations...

  var queries = new QueryServiceImpl();
  var evans = queries.getAuthorByUsername(AppConstants.E_EVANS);
  var blochBooks = queries.getAuthorBooks(AppConstants.J_BLOCH);
  LOGGER.info("Author evans : {}", evans);
  LOGGER.info("jBloch books : {}", blochBooks);
}

This separation allows the write model to enforce complex business rules and validation while the read model provides denormalized, query-optimized views of the data.

Key Benefits of CQRS Separation

Implementing CQRS pattern for command and query separation provides several architectural advantages:

  • Independent scalability: Deploy additional read replicas without impacting write throughput, or scale write nodes independently from query caches.
  • Optimized data models: The write model uses rich domain entities with encapsulation, while the read model uses flattened DTOs tailored for specific UI requirements.
  • Performance isolation: Long-running complex queries cannot block transactional writes, and vice versa.
  • Simplified evolution: Changes to query projections do not affect domain logic, allowing the read model to evolve rapidly without risking write-side integrity.

Summary

  • Command services handle state mutations through CommandServiceImpl, using Hibernate transactions to persist domain entities.
  • Query services retrieve data via QueryServiceImpl, executing HQL or native SQL to project lightweight DTOs without transactional overhead.
  • The command-query-responsibility-segregation module in iluwatar/java-design-patterns provides a complete, runnable example using a library domain (authors and books).
  • Separation enables independent scaling, performance optimization, and clearer architectural boundaries between read and write concerns.

Frequently Asked Questions

What is the difference between Command and Query in CQRS?

Commands are operations that modify application state—such as authorCreated or bookAddedToAuthor—and execute within transactions to ensure consistency. Queries are read-only operations, such as getAuthorByUsername or getAuthorBooksCount, that return data without side effects and typically bypass transactional wrappers for performance. In the java-design-patterns implementation, commands persist domain entities while queries return immutable DTOs from com.iluwatar.cqrs.dto.

When should I use CQRS pattern in Java applications?

Use CQRS when your application experiences divergent read and write workloads, requires different optimization strategies for queries versus commands, or when the read model needs to serve multiple presentation formats. The pattern excels in complex domains where write logic involves intricate validation rules, while read operations require denormalized, high-performance projections. Simple CRUD applications with symmetrical read/write patterns may not justify the added architectural complexity.

How does CQRS handle data consistency between read and write models?

The reference implementation uses a single database with separate service layers, ensuring strong consistency since both sides read from the same underlying tables. In distributed architectures, CQRS often adopts eventual consistency: the write model publishes domain events that asynchronously update read-model projections. The java-design-patterns example demonstrates the simpler synchronous approach, where queries immediately reflect committed command operations.

Can CQRS work with separate databases for reads and writes?

Yes, though the java-design-patterns example uses a single database, CQRS naturally supports polyglot persistence—using a relational database for transactional writes and a document store or search index for reads. This configuration requires an event-driven synchronization mechanism, typically using domain events from the command side to update the read-side database. The CommandServiceImpl could be extended to emit events after session.getTransaction().commit(), while QueryServiceImpl would query the separate read-optimized data store.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →