Skip to content

Command Query Separation (CQS)


Definition

Command Query Separation Definition


Violations

// VIOLATION 1: Query that modifies state
public class Stack<E> {

    // BAD: pop() is both query AND command
    public E pop() {
        E element = elements[--size];  // Modifies state
        return element;                 // Returns value
    }
}

// VIOLATION 2: Save that returns saved entity
public class UserRepository {

    // BAD: Command that returns data
    public User save(User user) {
        entityManager.persist(user);
        return user;  // Returns the saved user
    }
}

// VIOLATION 3: Method that does too much
public class OrderService {

    // BAD: Processes AND returns result
    public Receipt processPayment(Order order) {
        validateOrder(order);
        chargeCard(order);
        updateInventory(order);
        sendConfirmation(order);
        return generateReceipt(order);  // Mixed!
    }
}

Proper CQS

// GOOD: Separate Commands and Queries

public class Stack<E> {
    // QUERY: Returns top element without removing
    public E peek() {
        if (isEmpty()) throw new EmptyStackException();
        return elements[size - 1];
    }

    // COMMAND: Removes top element
    public void pop() {
        if (isEmpty()) throw new EmptyStackException();
        elements[--size] = null;
    }

    // Usage (two calls instead of one)
    E top = stack.peek();
    stack.pop();
}


public class UserRepository {
    // COMMAND: Saves user
    public void save(User user) {
        entityManager.persist(user);
    }

    // QUERY: Finds user
    public Optional<User> findById(String id) {
        return Optional.ofNullable(entityManager.find(User.class, id));
    }

    // Usage
    repository.save(user);
    User saved = repository.findById(user.getId()).orElseThrow();
}


public class OrderService {
    // COMMAND: Processes order
    public void processOrder(Order order) {
        validateOrder(order);
        chargeCard(order);
        updateInventory(order);
        sendConfirmation(order);
    }

    // QUERY: Gets receipt
    public Receipt getReceipt(String orderId) {
        Order order = orderRepository.findById(orderId).orElseThrow();
        return receiptGenerator.generate(order);
    }
}

Exceptions to CQS

CQS Exceptions


CQRS (Command Query Responsibility Segregation)

CQRS Architecture


CQS in Practice

// Real-world service design following CQS

public interface UserService {

    // ========== COMMANDS (return void) ==========

    void createUser(CreateUserCommand command);

    void updateProfile(UpdateProfileCommand command);

    void changePassword(ChangePasswordCommand command);

    void deactivateUser(String userId);

    // ========== QUERIES (return data) ==========

    Optional<UserDTO> findById(String id);

    List<UserDTO> findByRole(Role role);

    Page<UserDTO> search(UserSearchCriteria criteria, Pageable page);

    boolean existsByEmail(String email);

    UserStats getStatistics(String userId);
}

// Command objects (immutable)
public record CreateUserCommand(
    String email,
    String name,
    String password
) {}

public record UpdateProfileCommand(
    String userId,
    String name,
    String bio,
    String avatarUrl
) {}

// Implementation
@Service
public class UserServiceImpl implements UserService {

    @Override
    @Transactional
    public void createUser(CreateUserCommand command) {
        // Validate
        if (userRepository.existsByEmail(command.email())) {
            throw new EmailAlreadyExistsException(command.email());
        }

        // Execute command
        User user = new User(command.email(), command.name());
        user.setPassword(passwordEncoder.encode(command.password()));
        userRepository.save(user);

        // Side effects
        eventPublisher.publish(new UserCreatedEvent(user.getId()));

        // No return value - pure command
    }

    @Override
    @Transactional(readOnly = true)  // Optimization for queries
    public Optional<UserDTO> findById(String id) {
        return userRepository.findById(id)
            .map(this::toDTO);
        // No side effects - pure query
    }
}

Tips & Tricks

Tips and Tricks