ভূমিকা
আধুনিক সফটওয়্যার আর্কিটেকচারে CQRS একটি অত্যন্ত গুরুত্বপূর্ণ ডিজাইন প্যাটার্ন। বড় এবং জটিল অ্যাপ্লিকেশন তৈরি করার সময় এই প্যাটার্নটি কোডকে আরও পরিষ্কার, স্কেলেবল এবং মেইনটেইনেবল করে তোলে। এই টিউটোরিয়ালে আমরা Java ব্যবহার করে CQRS সম্পর্কে বিস্তারিত আলোচনা করব।
CQRS কী?
CQRS এর পূর্ণরূপ হলো Command Query Responsibility Segregation। এই প্যাটার্নটি মূলত একটি সাধারণ ধারণার উপর প্রতিষ্ঠিত:
"যে অপারেশন ডেটা পরিবর্তন করে (Command) এবং যে অপারেশন ডেটা পড়ে (Query) — তাদের আলাদা করে রাখো।"
সহজ ভাষায়:
- Command → ডেটা লেখে (Create, Update, Delete) — কোনো ডেটা রিটার্ন করে না
- Query → ডেটা পড়ে (Read) — কোনো ডেটা পরিবর্তন করে না
এই দুটি দায়িত্বকে আলাদা করাই হলো CQRS-এর মূল লক্ষ্য।
কেন CQRS ব্যবহার করবেন?
ঐতিহ্যবাহী CRUD (Create, Read, Update, Delete) অ্যাপ্লিকেশনে একটিই মডেল থাকে যা রিড এবং রাইট — উভয় কাজ করে। এটি ছোট প্রজেক্টে ঠিক থাকলেও বড় সিস্টেমে কিছু সমস্যা তৈরি করে:
- জটিলতা বৃদ্ধি: রিড এবং রাইট মডেল প্রায়শই আলাদা আলাদা ডেটা স্ট্রাকচার প্রয়োজন করে।
- পারফরম্যান্স সমস্যা: রিড অপারেশন প্রায়ই অনেক বেশি হয়, তাই আলাদাভাবে অপ্টিমাইজ করা দরকার।
- স্কেলেবিলিটি: রিড ও রাইট লোড আলাদাভাবে স্কেল করা কঠিন হয়।
- নিরাপত্তা: রিড ও রাইটের জন্য আলাদা পারমিশন লজিক রাখা ভালো।
CQRS এই সমস্যাগুলো সমাধান করে।
CQRS-এর মূল উপাদানসমূহ
১. Command (কমান্ড)
Command হলো একটি অনুরোধ যা সিস্টেমের state পরিবর্তন করে। এটি একটি intention বা ইচ্ছা প্রকাশ করে, যেমন — "একটি নতুন অর্ডার তৈরি করো" বা "পণ্যের দাম আপডেট করো"।
বৈশিষ্ট্যসমূহ:
- কোনো ডেটা রিটার্ন করে না (void বা শুধু success/failure)
- একটি নির্দিষ্ট কাজ করে
- নাম সাধারণত verb দিয়ে শুরু হয়, যেমন:
CreateOrderCommand,UpdateProductCommand
২. Query (কুয়েরি)
Query হলো একটি অনুরোধ যা সিস্টেম থেকে ডেটা পড়ে কিন্তু কোনো পরিবর্তন করে না।
বৈশিষ্ট্যসমূহ:
- সবসময় ডেটা রিটার্ন করে
- সিস্টেমের state পরিবর্তন করে না (side-effect free)
- নাম সাধারণত:
GetOrderByIdQuery,FindAllProductsQuery
৩. Command Handler (কমান্ড হ্যান্ডলার)
Command Handler হলো সেই ক্লাস যা একটি নির্দিষ্ট Command প্রসেস করে। প্রতিটি Command-এর জন্য একটি Handler থাকে।
৪. Query Handler (কুয়েরি হ্যান্ডলার)
Query Handler হলো সেই ক্লাস যা একটি নির্দিষ্ট Query প্রসেস করে এবং ডেটা রিটার্ন করে।
Java-তে CQRS বাস্তবায়ন
আমরা একটি Product Management সিস্টেম তৈরি করব যেখানে CQRS প্যাটার্ন ব্যবহার করা হবে।
প্রজেক্ট স্ট্রাকচার
src/
├── command/
│ ├── CreateProductCommand.java
│ ├── UpdateProductCommand.java
│ └── DeleteProductCommand.java
├── query/
│ ├── GetProductByIdQuery.java
│ └── GetAllProductsQuery.java
├── handler/
│ ├── command/
│ │ ├── CreateProductCommandHandler.java
│ │ ├── UpdateProductCommandHandler.java
│ │ └── DeleteProductCommandHandler.java
│ └── query/
│ ├── GetProductByIdQueryHandler.java
│ └── GetAllProductsQueryHandler.java
├── model/
│ ├── Product.java
│ └── ProductDTO.java
├── repository/
│ └── ProductRepository.java
└── bus/
├── CommandBus.java
└── QueryBus.java
ধাপ ১: Command ক্লাস তৈরি
// CreateProductCommand.java
public class CreateProductCommand {
private final String name;
private final String description;
private final double price;
private final int stock;
public CreateProductCommand(String name, String description, double price, int stock) {
this.name = name;
this.description = description;
this.price = price;
this.stock = stock;
}
public String getName() { return name; }
public String getDescription() { return description; }
public double getPrice() { return price; }
public int getStock() { return stock; }
}
// UpdateProductCommand.java
public class UpdateProductCommand {
private final Long id;
private final String name;
private final double price;
public UpdateProductCommand(Long id, String name, double price) {
this.id = id;
this.name = name;
this.price = price;
}
public Long getId() { return id; }
public String getName() { return name; }
public double getPrice() { return price; }
}
// DeleteProductCommand.java
public class DeleteProductCommand {
private final Long id;
public DeleteProductCommand(Long id) {
this.id = id;
}
public Long getId() { return id; }
}
ধাপ ২: Query ক্লাস তৈরি
// GetProductByIdQuery.java
public class GetProductByIdQuery {
private final Long id;
public GetProductByIdQuery(Long id) {
this.id = id;
}
public Long getId() { return id; }
}
// GetAllProductsQuery.java
public class GetAllProductsQuery {
// কোনো প্যারামিটার নেই — সব প্রোডাক্ট আনবে
}
ধাপ ৩: Model এবং DTO
// Product.java (Domain Model)
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String description;
private double price;
private int stock;
public Product() {}
public Product(String name, String description, double price, int stock) {
this.name = name;
this.description = description;
this.price = price;
this.stock = stock;
}
public Long getId() { return id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public double getPrice() { return price; }
public void setPrice(double price) { this.price = price; }
public int getStock() { return stock; }
public void setStock(int stock) { this.stock = stock; }
}
// ProductDTO.java
public class ProductDTO {
private Long id;
private String name;
private String description;
private double price;
private int stock;
public ProductDTO(Long id, String name, String description, double price, int stock) {
this.id = id;
this.name = name;
this.description = description;
this.price = price;
this.stock = stock;
}
public Long getId() { return id; }
public String getName() { return name; }
public String getDescription() { return description; }
public double getPrice() { return price; }
public int getStock() { return stock; }
}
ধাপ ৪: Repository
// ProductRepository.java
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
}
ধাপ ৫: Command Handlers
// CreateProductCommandHandler.java
@Service
public class CreateProductCommandHandler {
private final ProductRepository productRepository;
@Autowired
public CreateProductCommandHandler(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public void handle(CreateProductCommand command) {
if (command.getName() == null || command.getName().isEmpty()) {
throw new IllegalArgumentException("Product name cannot be empty");
}
if (command.getPrice() < 0) {
throw new IllegalArgumentException("Price cannot be negative");
}
Product product = new Product(
command.getName(),
command.getDescription(),
command.getPrice(),
command.getStock()
);
productRepository.save(product);
}
}
// UpdateProductCommandHandler.java
@Service
public class UpdateProductCommandHandler {
private final ProductRepository productRepository;
@Autowired
public UpdateProductCommandHandler(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public void handle(UpdateProductCommand command) {
Product product = productRepository.findById(command.getId())
.orElseThrow(() -> new RuntimeException("Product not found: " + command.getId()));
product.setName(command.getName());
product.setPrice(command.getPrice());
productRepository.save(product);
}
}
// DeleteProductCommandHandler.java
@Service
public class DeleteProductCommandHandler {
private final ProductRepository productRepository;
@Autowired
public DeleteProductCommandHandler(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public void handle(DeleteProductCommand command) {
if (!productRepository.existsById(command.getId())) {
throw new RuntimeException("Product not found: " + command.getId());
}
productRepository.deleteById(command.getId());
}
}
ধাপ ৬: Query Handlers
// GetProductByIdQueryHandler.java
@Service
public class GetProductByIdQueryHandler {
private final ProductRepository productRepository;
@Autowired
public GetProductByIdQueryHandler(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public ProductDTO handle(GetProductByIdQuery query) {
Product product = productRepository.findById(query.getId())
.orElseThrow(() -> new RuntimeException("Product not found: " + query.getId()));
return new ProductDTO(
product.getId(),
product.getName(),
product.getDescription(),
product.getPrice(),
product.getStock()
);
}
}
// GetAllProductsQueryHandler.java
@Service
public class GetAllProductsQueryHandler {
private final ProductRepository productRepository;
@Autowired
public GetAllProductsQueryHandler(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public List<ProductDTO> handle(GetAllProductsQuery query) {
return productRepository.findAll()
.stream()
.map(p -> new ProductDTO(p.getId(), p.getName(), p.getDescription(), p.getPrice(), p.getStock()))
.collect(Collectors.toList());
}
}
ধাপ ৭: Command Bus এবং Query Bus
// CommandBus.java
@Service
public class CommandBus {
private final CreateProductCommandHandler createHandler;
private final UpdateProductCommandHandler updateHandler;
private final DeleteProductCommandHandler deleteHandler;
@Autowired
public CommandBus(
CreateProductCommandHandler createHandler,
UpdateProductCommandHandler updateHandler,
DeleteProductCommandHandler deleteHandler
) {
this.createHandler = createHandler;
this.updateHandler = updateHandler;
this.deleteHandler = deleteHandler;
}
public void dispatch(Object command) {
if (command instanceof CreateProductCommand) {
createHandler.handle((CreateProductCommand) command);
} else if (command instanceof UpdateProductCommand) {
updateHandler.handle((UpdateProductCommand) command);
} else if (command instanceof DeleteProductCommand) {
deleteHandler.handle((DeleteProductCommand) command);
} else {
throw new IllegalArgumentException("Unknown command: " + command.getClass().getName());
}
}
}
// QueryBus.java
@Service
public class QueryBus {
private final GetProductByIdQueryHandler getByIdHandler;
private final GetAllProductsQueryHandler getAllHandler;
@Autowired
public QueryBus(
GetProductByIdQueryHandler getByIdHandler,
GetAllProductsQueryHandler getAllHandler
) {
this.getByIdHandler = getByIdHandler;
this.getAllHandler = getAllHandler;
}
@SuppressWarnings("unchecked")
public <T> T dispatch(Object query) {
if (query instanceof GetProductByIdQuery) {
return (T) getByIdHandler.handle((GetProductByIdQuery) query);
} else if (query instanceof GetAllProductsQuery) {
return (T) getAllHandler.handle((GetAllProductsQuery) query);
} else {
throw new IllegalArgumentException("Unknown query: " + query.getClass().getName());
}
}
}
ধাপ ৮: REST Controller
// ProductController.java
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final CommandBus commandBus;
private final QueryBus queryBus;
@Autowired
public ProductController(CommandBus commandBus, QueryBus queryBus) {
this.commandBus = commandBus;
this.queryBus = queryBus;
}
@PostMapping
public ResponseEntity<String> createProduct(@RequestBody CreateProductRequest request) {
commandBus.dispatch(new CreateProductCommand(
request.getName(), request.getDescription(), request.getPrice(), request.getStock()
));
return ResponseEntity.status(HttpStatus.CREATED).body("Product created successfully");
}
@PutMapping("/{id}")
public ResponseEntity<String> updateProduct(@PathVariable Long id, @RequestBody UpdateProductRequest request) {
commandBus.dispatch(new UpdateProductCommand(id, request.getName(), request.getPrice()));
return ResponseEntity.ok("Product updated successfully");
}
@DeleteMapping("/{id}")
public ResponseEntity<String> deleteProduct(@PathVariable Long id) {
commandBus.dispatch(new DeleteProductCommand(id));
return ResponseEntity.ok("Product deleted successfully");
}
@GetMapping("/{id}")
public ResponseEntity<ProductDTO> getProductById(@PathVariable Long id) {
ProductDTO product = queryBus.dispatch(new GetProductByIdQuery(id));
return ResponseEntity.ok(product);
}
@GetMapping
public ResponseEntity<List<ProductDTO>> getAllProducts() {
List<ProductDTO> products = queryBus.dispatch(new GetAllProductsQuery());
return ResponseEntity.ok(products);
}
}
CQRS এবং Event Sourcing
CQRS প্রায়ই Event Sourcing এর সাথে ব্যবহার করা হয়। Event Sourcing-এ সিস্টেমের প্রতিটি পরিবর্তন একটি Event হিসেবে সংরক্ষণ করা হয়।
// ProductCreatedEvent.java
public class ProductCreatedEvent {
private final Long productId;
private final String name;
private final double price;
private final LocalDateTime occurredAt;
public ProductCreatedEvent(Long productId, String name, double price) {
this.productId = productId;
this.name = name;
this.price = price;
this.occurredAt = LocalDateTime.now();
}
public Long getProductId() { return productId; }
public String getName() { return name; }
public double getPrice() { return price; }
public LocalDateTime getOccurredAt() { return occurredAt; }
}
Command Handler-এ Event publish করা:
@Service
public class CreateProductCommandHandler {
private final ProductRepository productRepository;
private final ApplicationEventPublisher eventPublisher;
@Autowired
public CreateProductCommandHandler(
ProductRepository productRepository,
ApplicationEventPublisher eventPublisher) {
this.productRepository = productRepository;
this.eventPublisher = eventPublisher;
}
public void handle(CreateProductCommand command) {
Product product = new Product(
command.getName(), command.getDescription(),
command.getPrice(), command.getStock()
);
Product saved = productRepository.save(product);
eventPublisher.publishEvent(
new ProductCreatedEvent(saved.getId(), saved.getName(), saved.getPrice())
);
}
}
Event Listener:
@Component
public class ProductEventListener {
@EventListener
public void handleProductCreated(ProductCreatedEvent event) {
System.out.println("Product created: " + event.getName() + " at " + event.getOccurredAt());
}
}
CQRS-এর সুবিধা ও অসুবিধা
সুবিধাসমূহ
- Single Responsibility: প্রতিটি ক্লাসের একটিই কাজ — হয় Command handle করা নয়তো Query।
- স্কেলেবিলিটি: Read এবং Write সাইড আলাদাভাবে স্কেল করা যায়।
- পারফরম্যান্স: Read মডেলকে Denormalized বা Cached করে রাখা যায় যা Query অনেক দ্রুত করে।
- নিরাপত্তা: Command ও Query আলাদা থাকায় আলাদা authorization rules প্রয়োগ করা সহজ।
- পরিষ্কার কোড: ব্যবসায়িক লজিক আরও স্পষ্ট এবং বোধগম্য হয়।
অসুবিধাসমূহ
- জটিলতা: ছোট প্রজেক্টে CQRS অতিরিক্ত জটিলতা যোগ করে।
- Eventual Consistency: Read ও Write আলাদা ডেটাবেজে থাকলে ডেটা সাথে সাথে sync নাও হতে পারে।
- বেশি কোড: প্রতিটি অপারেশনের জন্য আলাদা Command/Query এবং Handler লিখতে হয়।
কখন CQRS ব্যবহার করবেন?
CQRS সব প্রজেক্টে উপযুক্ত নয়। নিচের পরিস্থিতিতে CQRS ব্যবহার করা উচিত:
- যখন Read ও Write লোড উল্লেখযোগ্যভাবে আলাদা।
- যখন অ্যাপ্লিকেশন অনেক জটিল ব্যবসায়িক লজিক ধারণ করে।
- যখন Microservices আর্কিটেকচার ব্যবহার করা হচ্ছে।
- যখন Event Sourcing প্রয়োজন।
এড়িয়ে চলুন যখন:
- প্রজেক্ট ছোট বা সাধারণ CRUD অ্যাপ।
- টিম CQRS-এ অভিজ্ঞ নয়।
- দ্রুত প্রোটোটাইপ তৈরি করতে হবে।
সারসংক্ষেপ
CQRS একটি শক্তিশালী আর্কিটেকচারাল প্যাটার্ন যা Read ও Write অপারেশনকে আলাদা করে কোডকে আরও পরিষ্কার, স্কেলেবল এবং মেইনটেইনেবল করে তোলে। Java এবং Spring Boot-এ এটি বাস্তবায়ন করা বেশ সহজ।
মূল বিষয়গুলো মনে রাখুন:
- Command → ডেটা পরিবর্তন করে, কিছু রিটার্ন করে না
- Query → ডেটা পড়ে, কিছু পরিবর্তন করে না
- Handler → প্রতিটি Command/Query-র জন্য আলাদা Handler
- Bus → সঠিক Handler-এ Request পাঠায়
সঠিক পরিস্থিতিতে CQRS ব্যবহার করলে আপনার অ্যাপ্লিকেশনের মান এবং পারফরম্যান্স উল্লেখযোগ্যভাবে উন্নত হবে।