I wanted to get some feedback on the design of a chat application I recently developed. The application is written in PHP, but the language probably isn't important here.
The main components are User
, Conversation
, and Message
:
class User {
public function getId(): int {}
public function getUsername(): string {}
public function getFirstName(): string {}
public function getLastName(): string {}
// token used with a 3rd party API for sending messages
public function getToken(): ?string;
// if a user doesn't have a token they can't be messaged
public function isOnline(): bool {}
public function __construct(int $id, string $username, ...) {}
}
class Conversation {
public function getId(): int {}
public function getUsers(): User[] {}
public function __construct(int $id, array $users) {}
}
class Message {
public function getId(): int {}
public function getText(): string {}
public function getConversation(): Conversation {}
public function getAuthor(): User {}
public function __construct(int $id, string $text, Conversation $conversation) {}
}
I also have some services:
class MessageSender implements MessageSenderInterface
{
private LoggerInterface $logger;
public function send(Message $message): void {
foreach ($message->getConversation()->getUsers() as $user) {
if (!$user->isOnline()) {
$this->logger->warn('User is offline and cannot be messaged');
}
if ($user->equals($message->getAuthor())) {
// continue; don't send messages to authors
}
$messageData = [
'to' => $user->getToken(),
'from' => $message->getAuthor()->getUsername(),
'text' => $message->getText(),
];
// send the message through some external API
}
}
}
Most of the work is done through the MessageSender
, but I'm wondering if the domain might be better encapsulated with something like this:
class Message {
public function getId(): int {}
public function getText(): string {}
public function __construct(int $id, string $text, Conversation $conversation) {}
public function send(MessageSenderInterface $sender, LoggerInterface $logger) {
... send logic in here
}
}
You can see that by moving the send
functionality inside of the Message
object we totally get rid of two exposed properties (getConversation
and getAuthor
are gone) and could effectively remove the service altogether. But as a result, the message object now knows about loggers and message senders, even if they are just interfaces.
What does DDD say about this? I tend to prefer exposing less data and like the encapsulation the second option provides.
Best Answer
Some DDD practitioners suggest that it is ok to inject technology layers dynamically into your domain model. Modeled that way, your domain code will not be directly dependent on technology components and only talk through abstract interfaces.
But I would suggest that you keep only the domain related code in the domain model layer and organize all technology interactions (DB, Message Brokers, Loggers) in your service layer. These are typically Application Services and Command Handlers in the DDD/CQRS lingo.
Here are some reasons why placing code that interacts with technology components in the domain model is probably a bad practice:
Mapping this thought process to your example, the logic to decide whether to send the message would be in the domain layer. The code to format the event data and dispatch to the message broker would be in the service layer.
On a separate note, one wouldn't organize these three components (User, Conversation, and Message) in this way in a DDD application. You would think about transaction boundaries and create
aggregates
around data objects.A
User
would be an aggregate, with its own enclosed objects and behavior. AConversation
would be another aggregate and encloseMessage
objects within it, and all message related interactions would be via the Conversation aggregate.Because they are separate aggregates, you would not embed
User
objects into aConversation
aggregate. You would only have references (user identifiers). You would have a read model that tracks which users are online in a Conversation and use it to dispatch messages.I suggest you go through the EventSourcery course for a good understanding of these concepts. The code for the course is actually in PHP.
Update 1:
Your
Message
object is reaching back to theConversation
object to gather users to do its work, so it makes sense to enclose it within theConversation
object.I will talk about two concepts that may not be part of your architecture right now but would help: Application Services and Domain Events.
You would introduce an intermediate "Application Service" layer between your controller and the Domain Layer.
The App Service will be responsible for invoking the (injected) infrastructure services, calling the domain layer, and persisting/loading necessary data. The Controller's responsibility is only to gather the request params (gather user input in your case), ensure authentication (if needed), and then make the call to the Application Service method.
Application Services are the direct clients of the domain model and act as intermediaries to coordinate between the external world and the domain layer. They are responsible for handling infrastructure concerns like ID Generation, Transaction Management, Encryption, etc. Such responsibilities are not concerns of the Controller layer either.
Let's assume
MessageSender
is transformed into an Application Service. Here is an example control flow:conversation_id
,user_id
(author), andmessage
.Conversation
from the database. If the Conversation ID is valid, and the author can participate in this conversation (these are invariants), you invoke asend
method on theConversation
object.events
, to be dispatched into a message interface (these are collected in a temporary variable valid for that session) and returns. These events contain the entire data to reconstruct details of the message (timestamps, audit log, etc.), and don't just cater to what is pushed out to the receiver later.With this structure, you have a good implementation of the Open-Closed Principle.
Update 2: Pseudocode
Application Service:
Domain Model: