I have two aggregates: Book and Reader. One reader can borrow up to 3 books. When a read borrows a book, I need to add this book to his borrowed book list, and the same time I need to update the book to save the reader as the current borrower.
This design looks ok. In this case, a command only affects a single aggregate. That one command produces an event that is handled by two components to update two query models. It’s better to be really clear about the role of your classes. Use the suffix Command and Event for the respective types of classes, so that you don’t mix them up.
Howmany transaction are involved depends on the infrastructure components. 1 command is 1 transaction. If you handle events asynchronously, it’s an extra transaction for each async handling. Conceptually, each message is its own transaction. If you don’t rely on multiple messages (command and resulting event handling) being handled in the same transaction, you achieve the best flexibility.