P-Chat clean architecture

Building P‑CHAT: A Real-Time Chat App with Clean Architecture

An in-depth look at P‑CHAT’s structure, using Clean Architecture to build a scalable, testable, and secure chat system.


🔰 Introduction

P‑CHAT is a proof-of-concept for a real-time chat application developed with Clean Architecture principles. It combines a modern frontend (SPA) with a NestJS backend and supports HTTP, WebSocket, and end-to-end encryption (E2EE).

The source code is available here: github.com/moez-sadok/chat-clean-architecture

📚 References

The P‑CHAT architecture is inspired by the foundational principles described by Robert C. Martin (Uncle Bob):

🔗
The Clean Architecture – Uncle Bob (cleancoder.com)

🧱 Component Diagram & Architecture Layers

The P‑CHAT architecture follows Clean Architecture by organizing code into clear concentric layers:

📌 Note: Color usage in the diagram follows a strict legend to visually separate Clean Architecture layers. This color coding respects the Clean Architecture convention and is consistently applied across all diagrams in the repository.

  • Entities – Core business logic (e.g. Message, User, Room)
  • Use Cases – Application-specific logic (e.g. send message, join room)
  • Adapters – Controllers, Presenters, Gateways, Views
  • Frameworks & Drivers – External technologies: database, storage, web server, crypto libraries

This separation helps enforce dependency rules and keeps the system modular, scalable, and testable.

Entities

Entities like Message, Participant, and Room are the most abstract part of the application. They are free of dependencies and express core business rules.

📌 Design Note: The system uses the Mediator pattern, where the Room acts as the mediator coordinating message flow between Participants (colleagues).

🧠 Application Core (Use Cases)

In this architecture, each business operation is encapsulated within a dedicated UseCase Interactor, which serves as the execution unit for a specific application behavior — such as sending a message, joining a room, or get user rooms.

📌 Note: While using the I prefix for interfaces (e.g., IRepository, IPresenter) is not generally recommended in modern software conventions, it is applied here intentionally to clarify the distinction between abstractions and implementations—especially in diagrams and educational contexts.

The Usecase Interactor acts as the central coordinator, implementing the use case (InputPort / Requester) by:

  • Validating inputs received from controllers (HTTP or WebSocket),
  • Accessing and persisting data through the abstract PersistenceGateway (IRepository),
  • Applying domain logic by interacting with core Entities,
  • Formatting results via the Presenter (OututPort), which transforms the output into a UI-ready format consumed by the View.

This design guarantees that use cases are isolated from frameworks, transport mechanisms, and UI layers by depending solely on abstract interfaces (ports).
As a result, the application layer remains clean, modular, and highly testable — fully decoupled from concerns such as storage engines, communication protocols, or presentation frameworks.

Input and Output Boundaries

Controllers send data to the use cases through input boundaries, while output is returned through presenters. This enforces direction of dependencies from the outside-in.

🔜 A dedicated article exploring controller, presenter, and view adapters — including HTTP, WebSocket, SSR, SPA, and API — will be published soon.

🌐 HTTP Communication

P‑CHAT uses HTTP for key actions such as:

  • User login, logout, and authentication
  • Fetching room or message history
  • Sending messages (by default or when WebSocket is unavailable)

The flow:

  1. The SPA client initiates a request using the HttpClientAdapter, typically via a service or effect (e.g. login, send message).
  2. The request is received by the NestJS HttpServerAdapter, which serves as the transport-layer entry point on the backend.
  3. The corresponding HTTP controller parses the incoming request and delegates the execution to the appropriate UseCase within the application layer.
  4. Once the UseCase completes, a Presenter formats the result into a ViewModel—a structure optimized for frontend rendering.
  5. The frontend’s View observes changes in the ViewModel, triggering UI updates, and the response is received via the HttpClientAdapter.

🔄 WebSocket Communication

WebSockets power full-duplex messaging:

– Clients use WebSocketClientAdapter
– Server responds via WebSocketServerAdapter
– Use cases handle the logic via WebSocketController

🗄️ Data Persistence and Repository Gateway

To keep the business logic independent from how data is stored or retrieved, the application relies on clear interface boundaries. This way, the core logic works with abstract contracts and stays completely unaware of the underlying database or storage technology.

As shown in the diagram:

  1. The Interactor (Use Case) communicates with the data layer via the IRepository interface.
  2. The concrete Repository implements this interface and serves as a bridge to the actual data source.
  3. It calls the IDataSource, which performs queries or commands against the DomainDataBase (SQL, NoSQL, or others).
  4. The DataMapper transforms raw data into rich Entities, ensuring domain integrity.
  5. The Interactor receives fully mapped, framework-agnostic domain objects.

This structure ensures that the application remains modular and testable, and that any storage technology can be replaced without affecting the business logic.

🔐 End-to-End Encryption (E2EE)

Before a message leaves the client, it is encrypted using browser-native APIs such as WebCryptoApi or third-party libs like libsodium.

The EncryptionService handles this on the client side. The backend simply stores and routes encrypted data — it never sees the decrypted content.
This design ensures strong privacy: only sender and recipient can decrypt messages.

🗃️ Storage, Cache & Notifications

  • MongoDB/Sql is used for chat and user data persistence
  • Redis supports state, pub/sub, and caching
  • NotificationGateway connects to email or push systems
  • LocalStorage and FileSystem are used for offline caching and storing local certificate files

🚀 Deployment as a Detail

One of the key ideas in Clean Architecture is that deployment is a detail. This means the architecture doesn’t dictate where a use case must run — whether on the server, in the browser, or on a mobile device — as long as the code respects the separation of concerns and architectural boundaries.

In our chat application, for example, we can implement end-to-end encryption (E2EE) by introducing use cases like EncryptMessageUseCase and DecryptMessageUseCase, which are executed on the frontend (SPA). These use cases are pure application logic (interactors), free of any framework or platform-specific code. They depend only on abstract interfaces like EncryptionService, and can be easily tested, extended, or replaced.

Similarly, PersistenceGateway as an interface can be implemented using browser storage mechanisms such as LocalStorage, IndexedDB, or even the client’s filesystem (e.g., for managing certificates).

To orchestrate this, we can add a new interactor such as SendMessageE2EEInteractor. This approach follows the Open/Closed Principle (OCP): our system is open for extension, but closed for modification. We can introduce new behaviors (like E2EE support) by plugging in new components that reuse or wrap existing use cases.

By treating deployment location (frontend vs. backend) as a technical detail and keeping logic modular, we preserve the clarity, testability, and scalability that Clean Architecture was designed for — even in advanced scenarios like secure messaging.

🧩 Clean Architecture Benefits

  • 🔍 Testability:
    Thanks to clear separation of concerns and dependency inversion, you can test use cases and business logic in isolation — without requiring a running server, database, or front-end environment.
  • 🧱 Independence:
    Each layer is decoupled from specific frameworks, databases, or delivery mechanisms. You can replace the UI (e.g., switch from Angular to React), change the database (e.g., SQL to NoSQL), or move from REST to GraphQL — without rewriting the core business logic.
  • 📦 Modularity:
    Components follow the Single Responsibility Principle (SRP), making them easier to understand, maintain, and evolve. This also encourages clean code organization and team autonomy.
  • 🔌 Flexibility:
    New delivery mechanisms (such as WebSocket, GraphQL, gRPC, or CLI) can be integrated by simply adding new adapters — without modifying core use cases or entities.
  • 📐 Scalability:
    The architecture is well-suited for growing systems with many features, contributors, or long-term maintenance needs. Its structure promotes refactoring and onboarding new developers without technical debt accumulation.
  • 🚀 Reusability:
    Core domain logic and use cases can be reused across multiple interfaces — for example, web, mobile, or batch jobs — by plugging in different adapters around the same application core.
  • 📚 Maintainability:
    Because logic is isolated and clearly layered, bug fixing, updates, and feature additions are safer and less likely to introduce side effects in unrelated parts of the system.

🧱 Limitations and Trade-offs of Clean Architecture

While Clean Architecture provides strong benefits like separation of concerns, testability, and long-term maintainability, it also comes with certain drawbacks to consider:

  • 🚧 Increased Complexity for Small Projects:
    The layered structure and strict interface separation can feel excessive for small or short-term applications. Simpler architectures might offer faster results in such contexts.
  • 📁 More Boilerplate Code:
    Maintaining independence between layers often requires writing extra interfaces, DTOs, and mappers — increasing code volume and effort.
  • 🧠 Steeper Learning Curve:
    Clean Architecture introduces abstract concepts like input/output ports, entities, SOLID principles, OOP Design Patterns and dependency inversion. These can be challenging for teams new to the approach, especially for junior developers.
  • 🕰️ Slower Initial Velocity:
    Projects may start slower due to the time spent designing boundaries and writing abstraction layers. However, this upfront cost usually pays off in the long term.
  • 🧩 Framework-Agnostic by Design — But Requires Discipline:
    While decoupling from frameworks provides flexibility, it also means more manual wiring and strict adherence to architecture principles. Teams must stay disciplined to avoid shortcutting the design.

✅ Conclusion

P‑CHAT demonstrates how Clean Architecture can be successfully applied to a modern, real-time, and secure chat application. By clearly separating concerns and depending only on abstractions, the system remains flexible, testable, and maintainable.

This architectural discipline allows the project to evolve safely and scale across new interfaces or platforms, all while preserving the integrity of the application core. Clean Architecture doesn’t just bring structure — it empowers growth and long-term maintainability.

Moreover, this example includes most of the essential components typically required in real-world web or mobile business applications — such as delivery platforms, e-commerce, banking systems, IoT dashboards, ride-hailing services, and more. It serves as a solid foundation for building complex systems that demand real-time communication, reliable data persistence, and secure user interactions.


About the Author
Moez SADOK – Founder at Prodsoft, passionate about scalable software, modular design, and Clean Architecture in real-world projects.

Leave A Comment