Deep Dive into Domain-Driven Design (DDD) with NatterNet
How We Sculpted a Real-Time Chat Application Using the Principles of DDD
In today's software landscape, an architectural approach that stands out for its focus on domain logic is Domain-Driven Design (DDD). In this post, we're taking you through a deep dive into DDD, and how we've successfully implemented this methodology in NatterNet, our real-time chat application built with Golang, MongoDB, WebSockets, Fiber, and NATS.
What is Domain-Driven Design?
Domain-Driven Design is an approach to software development that emphasizes a strong alignment between the application's core functionality (the "domain") and the design of the system. The methodology encourages developers to work closely with domain experts, to prioritize a "ubiquitous language" understood by both developers and stakeholders, and to identify strategic and tactical patterns that can be employed to solve complex business problems.
Ubiquitous Language
A strong point of focus in DDD is the ubiquitous language that bridges the gap between developers and non-developers. For example, in NatterNet, terms like "Room", "Message", and "User" are part of this language and are understood uniformly across all discussions and documentations.
Aggregates and Entities in NatterNet
NatterNet employs aggregates to ensure that all changes to data are consistent. For example, the Room
aggregate ensures that no two users with the same name can exist in the same room.
Entities in NatterNet like Message
, User
, and Room
have their own lifecycles and are uniquely identifiable, complying with DDD principles.
Separation of Concerns in NatterNet
One of the cornerstones of DDD is the explicit separation of concerns, and this principle is fully embraced in NatterNet.
Domain Services: These encapsulate domain logic that doesn't naturally fit within an entity. For example, in NatterNet, if the operation of sending a message involves complex business rules that are independent of the Message
entity itself, those rules would be a part of a domain service.
Application Services: These serve as the entry points to the domain layer from the outside world. They orchestrate the domain objects but do not contain business logic themselves.
Repositories: These interface with the Infrastructure Layer, providing a way for the domain to communicate with the data source without having to know how data storage is implemented.
Value Objects: These are immutable objects like UserID
or RoomID
, which don't have a lifecycle but are crucial to the understanding of the domain
Inter-Domain Communication: Event-Driven Architecture
In NatterNet, different domains communicate through an event-driven architecture facilitated by NATS. When a significant state change occurs in one domain, it emits an event. Other domains can subscribe to these events and act accordingly, maintaining loose coupling.
For instance, when a new message is sent in a chat room, the Message
domain might emit a NewMessageAdded
event. The Notification
domain, which is subscribed to this event, would then take the necessary steps to notify the users involved.
Intra-Domain Communication: Domain Context
Communication within the same domain context in NatterNet is typically more straightforward and often happens through method calls between aggregates and entities, or through domain services.
For example, within the Chat
domain, a Message
entity might have a method CreateMessage
that interacts with the Room
entity - to query the room, if exists. This interaction remains within the same domain context, and there is no need for emitting and listening to events.
CQRS and Event Sourcing in NatterNet
Another advanced DDD pattern implemented in NatterNet is Command Query Responsibility Segregation (CQRS) combined with Event Sourcing. Commands change the state but do not return data, while Queries return data and do not change the state. This separation allows for greater flexibility and scalability.
Conclusion
Domain-Driven Design offers an enriching and structured approach to software development, and its full potential is leveraged in NatterNet. By separating concerns, enabling inter-domain and intra-domain communication through event-driven architecture and domain context, NatterNet serves as an excellent example of DDD in action.
For more insights and to contribute, feel free to visit NatterNet on GitHub.
Stay tuned for future deep dives, where we will continue to explore other architectural patterns and methodologies that power NatterNet.