Skip to content

4+1 Architecture for Multi-Channel Comment & Thread System

Logical View

This diagram illustrates the high-level structure of core services and their interactions with external communication channels and internal components.

Core SystemEvent SystemInbound ChannelsComment ServiceThread ManagerNotification DispatcherDatabaseSlack ConnectorEmail ConnectorWhatsApp ConnectorTeams ConnectorMessage RouterSignalR Service
Core SystemEvent SystemInbound ChannelsComment ServiceThread ManagerNotification DispatcherDatabaseSlack ConnectorEmail ConnectorWhatsApp ConnectorTeams ConnectorMessage RouterSignalR Service

Process View

Comment Creation Flow

This flow describes how a user creates a comment, which is then distributed to external channels.

UserComment ServiceEvent Bus .Service Bus.Notification DispatcherSlack ConnectorWhatsApp ConnectorEmail ConnectorTeams ConnectorSignalR ServiceUserUserComment ServiceComment ServiceEvent Bus (Service Bus)Event Bus (Service Bus)Notification DispatcherNotification DispatcherSlack ConnectorSlack ConnectorWhatsApp ConnectorWhatsApp ConnectorEmail ConnectorEmail ConnectorTeams ConnectorTeams ConnectorSignalR ServiceSignalR ServicePOST /commentStore in DBPublish CommentCreated eventReceive CommentCreatedSend to SlackSend to WhatsAppSend to EmailSend to TeamsNotify UI clients
UserComment ServiceEvent Bus .Service Bus.Notification DispatcherSlack ConnectorWhatsApp ConnectorEmail ConnectorTeams ConnectorSignalR ServiceUserUserComment ServiceComment ServiceEvent Bus (Service Bus)Event Bus (Service Bus)Notification DispatcherNotification DispatcherSlack ConnectorSlack ConnectorWhatsApp ConnectorWhatsApp ConnectorEmail ConnectorEmail ConnectorTeams ConnectorTeams ConnectorSignalR ServiceSignalR ServicePOST /commentStore in DBPublish CommentCreated eventReceive CommentCreatedSend to SlackSend to WhatsAppSend to EmailSend to TeamsNotify UI clients

Reply from WhatsApp

This flow illustrates how a reply from WhatsApp is processed and synchronized across all platforms.

WhatsApp WebhookWhatsApp ConnectorMessage RouterComment ServiceEvent Bus .Service Bus.Notification DispatcherSlack ConnectorEmail ConnectorTeams ConnectorSignalR ServiceWhatsApp WebhookWhatsApp WebhookWhatsApp ConnectorWhatsApp ConnectorMessage RouterMessage RouterComment ServiceComment ServiceEvent Bus (Service Bus)Event Bus (Service Bus)Notification DispatcherNotification DispatcherSlack ConnectorSlack ConnectorEmail ConnectorEmail ConnectorTeams ConnectorTeams ConnectorSignalR ServiceSignalR ServiceIncoming MessageForward with metadataCreate Comment (reply)Publish CommentCreatedReceive CommentCreatedSend to SlackSend to EmailSend to TeamsNotify UI clients
520. plantuml.com | 520: Web server is returning an unknown error

Web server is returning an unknown error Error code 520

Visit cloudflare.com for more information.
2025-08-13 11:03:58 UTC
You

Browser

Working
London

Cloudflare

Working
www.plantuml.com

Host

Error

What happened?

There is an unknown connection issue between Cloudflare and the origin web server. As a result, the web page can not be displayed.

What can I do?

If you are a visitor of this website:

Please try again in a few minutes.

If you are the owner of this website:

There is an issue between Cloudflare's cache and your origin web server. Cloudflare monitors for these errors and automatically investigates the cause. To help support the investigation, you can pull the corresponding error log from your web server and submit it our support team. Please include the Ray ID (which is at the bottom of this error page). Additional troubleshooting resources.

Development View

This view presents a Hexagonal Architecture with core domain services decoupled from external integrations.

520. plantuml.com | 520: Web server is returning an unknown error

Web server is returning an unknown error Error code 520

Visit cloudflare.com for more information.
2025-08-13 11:03:58 UTC
You

Browser

Working
London

Cloudflare

Working
www.plantuml.com

Host

Error

What happened?

There is an unknown connection issue between Cloudflare and the origin web server. As a result, the web page can not be displayed.

What can I do?

If you are a visitor of this website:

Please try again in a few minutes.

If you are the owner of this website:

There is an issue between Cloudflare's cache and your origin web server. Cloudflare monitors for these errors and automatically investigates the cause. To help support the investigation, you can pull the corresponding error log from your web server and submit it our support team. Please include the Ray ID (which is at the bottom of this error page). Additional troubleshooting resources.

509.

Physical View

This diagram illustrates the Azure-based infrastructure and external services.

520. plantuml.com | 520: Web server is returning an unknown error

Web server is returning an unknown error Error code 520

Visit cloudflare.com for more information.
2025-08-13 11:03:58 UTC
You

Browser

Working
London

Cloudflare

Working
www.plantuml.com

Host

Error

What happened?

There is an unknown connection issue between Cloudflare and the origin web server. As a result, the web page can not be displayed.

What can I do?

If you are a visitor of this website:

Please try again in a few minutes.

If you are the owner of this website:

There is an issue between Cloudflare's cache and your origin web server. Cloudflare monitors for these errors and automatically investigates the cause. To help support the investigation, you can pull the corresponding error log from your web server and submit it our support team. Please include the Ray ID (which is at the bottom of this error page). Additional troubleshooting resources.

AzureFastAPI AppAzure PostgreSQLAzure SignalR ServiceAzure Service BusSlackWhatsApp API (ACS/Twilio)SendGrid/Email APIMicrosoft TeamsAzure Function - DispatcherAzure Functions - Connectors

Thanks! I’ll create a 4+1 architectural view for the feature extension that supports distributed comment threads across Slack, WhatsApp, Teams, and Email. This will include event-driven messaging, API/webhook integrations, Azure-based infrastructure, and FastAPI/Postgres backend.

I’ll organize the views as follows: - Logical View: Core components and their responsibilities (e.g., Comment Service, Notification Router, Channel Connectors) - Process View: Interaction and asynchronous event flows using producers/consumers - Development View: Code structure, tech stack, and boundaries (aligning with Ports & Adapters) - Physical View: Azure deployment diagram (containers, SignalR, queues, databases) - Scenarios (Use Cases): End-to-end flows for making a comment, receiving replies, maintaining threads, etc.

I’ll get this outline ready for you shortly.

4+1 Architecture for Multi‑Channel Comment & Thread System

Overview: We propose an extended comment threading system where comments are broadcast to multiple external channels and replies from those channels sync back into a unified thread. The design follows the 4+1 architectural view model (The 4+1 Architecture Model: A Comprehensive Guide for Software Teams | by Maurice Lichtenberg | Feb, 2025 | Medium) (The 4+1 Architecture Model: A Comprehensive Guide for Software Teams | by Maurice Lichtenberg | Feb, 2025 | Medium), addressing logical structure, concurrency processes, development (implementation) structure, physical deployment, and key use-case scenarios. The solution leverages an event-driven approach for loose coupling and scalability, ensuring real-time updates via SignalR and seamless integration with Slack, WhatsApp, Email, and Teams.

Logical View

This view defines the major software components and their interactions, focusing on functionality rather than deployment (The 4+1 Architecture Model: A Comprehensive Guide for Software Teams | by Maurice Lichtenberg | Feb, 2025 | Medium). The core components of the extended comment/thread system are:

  • Comment Service (Core) – The central FastAPI-based service that manages comments and threads. It exposes RESTful endpoints for creating comments, replies, retrieving threads, etc. This service encapsulates domain logic for comment creation, linking replies to parent comments, and handling mentions. When a new comment or reply is posted via the app, the Comment Service stores it in PostgreSQL and emits a CommentCreated event to notify other components (decoupling creation from notification) (Event-driven architecture style - Azure Architecture Center | Microsoft Learn). It also handles comment retrieval and formatting for the UI. If a comment contains user mentions (identified by mentions relationship (thread feature description.md)), it flags those for special notification (see Mention handling below).

  • Thread Manager – A domain component (could be part of the Comment Service or a closely related module) responsible for maintaining thread consistency. It ensures each reply is correctly linked to a parent comment (using the parent_id in the data model (thread feature description.md)) and that the entire conversation history can be retrieved. The Thread Manager might also format threads for different channels (e.g., assembling a conversation summary) and handle mention logic (e.g., resolving @username in content to user IDs and populating the mentions list (thread feature description.md)). It works closely with the Comment Service to enforce business rules (such as preventing empty comments, as shown in the validation logic (thread feature description.md)).

  • Notification Dispatcher – An asynchronous component that receives events (e.g. NewCommentPosted, NewReplyPosted, UserMentioned) and orchestrates message delivery to external channels. It acts as the producer/consumer decoupling point: the Comment Service produces an event to an event bus or queue, and the Notification Dispatcher consumes it (Event-driven architecture style - Azure Architecture Center | Microsoft Learn). The dispatcher examines the event details (which comment, who is the author, which channels to notify, which users were mentioned, etc.) and then fans out notifications to the appropriate channels via the Channel Connectors. For example, if a comment is marked to go to Slack and Email, the dispatcher will invoke the Slack Connector and Email Connector. By using a publish-subscribe model, the Comment Service doesn’t need to know which channels will receive the message, keeping it decoupled (Event-driven architecture style - Azure Architecture Center | Microsoft Learn). The Notification Dispatcher can be implemented to handle high volumes by batch processing or parallel invocations for each channel.

  • Channel Connectors – A collection of integration adapters, one per external channel (e.g., Slack Connector, WhatsApp Connector, Email Connector, Teams Connector). Each connector knows how to interface with its respective platform, handling both outbound and inbound communication:

  • Outbound (sending messages): Connectors expose an API (or are invoked by the dispatcher) to send a comment notification into the channel. For Slack, the connector uses Slack’s Web API or Incoming Webhook to post the message (with support for Slack threading) (Sending messages using incoming webhooks - Slack API). For WhatsApp, it uses an API (e.g., Twilio API or Azure Communication Services) to send a WhatsApp message. For Email, it uses an email service (like SendGrid via SMTP or API) to send out an email notification. For Teams, it can use the Microsoft Graph API or Bot Framework to post a message into a Teams channel or chat.
  • Inbound (receiving replies): Connectors also handle incoming webhooks or API calls from the external platforms. For Slack, the Events API can be used – the Slack Connector provides a webhook endpoint that Slack calls on new messages (e.g., a user replied in a Slack thread) (Events API | Slack). WhatsApp replies are received via a webhook from the messaging API provider (Twilio or ACS will send an HTTP POST to our service when a WhatsApp message comes in). Email replies can be handled via an inbound email hook (for instance, using SendGrid’s inbound parse webhook to receive the reply email and forward it as an HTTP request to our system). Teams messages can be received via a Bot Framework endpoint or a Graph subscription/webhook. Each connector translates the incoming channel-specific payload (e.g., Slack’s JSON for a message event, or an email’s content) into a neutral format understood by our system.

  • Message Router – The component responsible for routing incoming messages from Channel Connectors into the core Comment/Thread system. When an external reply comes in (through a Connector), the Message Router determines which thread and user the message corresponds to, then delegates to the Comment Service to create a new comment in that thread. For routing, it uses metadata embedded in the messages or context from the channel:

  • For example, when the Notification Dispatcher sends out a Slack message, it might include a hidden reference ID or use Slack thread timestamps. The Slack Connector could store a mapping from Slack’s thread_ts to our internal Thread ID. Then, on an incoming Slack event for a reply (with a thread_ts linking to the original message), the Message Router looks up the corresponding thread and creates the reply comment in that thread.
  • Similarly, for WhatsApp or Email, the system could embed a thread identifier in the message (e.g., in an email’s reply-to address or in the WhatsApp message text as an ID or using Twilio’s session context). The Message Router uses that identifier to find the correct parent comment or thread in the database, ensuring the reply is attached to the right conversation.
  • The Message Router abstracts this logic away from individual connectors. Each connector hands off the incoming message to the router with whatever identifiers or metadata are available, and the router contains the logic to validate and map it to a known thread. If the sender on the external channel corresponds to a registered user in the system, the router also maps the external identity to an internal user (e.g., mapping a Slack user ID or an email address to our User.id). Once the router determines the proper parent_id and user_id, it calls the Comment Service to create a new Comment in the thread (persisting it to PostgreSQL) and emits a NewCommentPosted event for this reply. This event then goes back to the Notification Dispatcher, which will broadcast the reply to all other channels (excluding the origin channel to avoid echo). In this way, the thread stays synchronized across all platforms: every new message, regardless of source, is saved in one place and propagated out to everyone.

  • Mention Handler – (Optional sub-component) Part of the core logic that deals with user mentions in comments. When a comment is created or a reply comes in, this logic checks the content for any @username syntax or IDs. Mentioned users are recorded in the mentions association table (thread feature description.md), and the Notification Dispatcher can send those users a targeted notification (for example, a direct Slack message, an email, or a push notification) indicating they were mentioned. This ensures that even if a user isn’t actively following a thread on any channel, they are alerted to mentions. The mention notification will often include a secure link to view the comment or reply (ensuring only authorized users can access it).

All these components interact logically to deliver a seamless multi-channel experience. For instance, the Comment Service and Thread Manager ensure the single source of truth (the database) for all comments and threads, while the Notification Dispatcher and Channel Connectors ensure that every event in the system is reflected out to the channels (outbound) and vice versa (inbound), thus keeping conversations in sync. The design inherently supports extensibility – new channels can be added by introducing a new connector (adapter) without changing the core logic, thanks to the decoupled, interface-driven design.

Process View

The process view describes the dynamic behavior and concurrent aspects of the system, focusing on how components interact through runtime events and how the system handles workflows (The 4+1 Architecture Model: A Comprehensive Guide for Software Teams | by Maurice Lichtenberg | Feb, 2025 | Medium) (The 4+1 Architecture Model: A Comprehensive Guide for Software Teams | by Maurice Lichtenberg | Feb, 2025 | Medium). We illustrate the key asynchronous flows: (A) Comment creation broadcast and (B) External reply ingestion. The system uses an event-driven pipeline with a publish-subscribe model to handle these flows, providing loose coupling and scalability (Event-driven architecture style - Azure Architecture Center | Microsoft Learn) (Event-driven architecture style - Azure Architecture Center | Microsoft Learn).

A. Comment Creation & Multi-Channel Notification: When a user posts a comment via the web UI (or our app’s front-end):

  1. API Request: The front-end calls the FastAPI endpoint (Comment Service) to create a new comment (including content, author user_id, optional parent_id if it’s a reply, and which channels to notify or which users to mention).
  2. Persist & Event Publish: The Comment Service logic validates and saves the comment to PostgreSQL (ensuring it has a new UUID, timestamp, etc.). After saving, it emits a CommentCreated event (e.g., by pushing a message onto an Azure Service Bus topic or Azure Event Grid) containing the comment details (comment ID, content, author, parent_id, mentions, target channels, etc.). This event is the trigger for the notification workflow. The API response can return immediately to the user (they see their comment in the UI) while the notification dispatch happens asynchronously.
  3. Notification Dispatch: The Notification Dispatcher (running as an event consumer) is listening on the event bus for new comment events. Upon receiving the CommentCreated event, it processes it (possibly on a background worker or Azure Function). It determines which channels need to be notified. This could depend on the comment or thread settings (e.g., the user selected Slack and Email for this comment) and user preferences (e.g., subscribers to the thread and their preferred channels).
  4. Fan-out to Channels: For each target channel, the dispatcher invokes the corresponding Channel Connector:
  5. Slack: The Slack Connector is called with the message content and thread context. It uses Slack’s HTTP Web API to post the message to a Slack channel or DM. If the comment is a reply to an existing thread, it includes Slack’s thread_ts of the parent message to post it as a threaded reply so that Slack users see it in context (Finding your way around the world of bots as a web developer | by Shay DeWael | Slack Platform Blog | Medium). If it’s a new thread, the connector posts a new message (and captures its ts for future replies). The message might include a reference to view the comment in the web app (a URL) and mention any Slack users if applicable.
  6. WhatsApp: The WhatsApp Connector takes the comment and sends it via an external API. For instance, using Twilio’s API or Azure Communication Services (ACS) to send a WhatsApp message to a user or group. The message includes the comment text and possibly a short link to view or reply. (ACS enables sending WhatsApp messages through a managed service (Part 1 - Multichannel Notification System with Azure Communication Services and Azure Functions | Microsoft Community Hub).) The connector ensures the message is formatted for WhatsApp (plain text or WhatsApp templated message).
  7. Email: The Email Connector formats an email (e.g., subject might be “New Comment in Thread X”) and sends it via an email service (like Twilio SendGrid or ACS Email). The email contains the comment content, author, and a link or instructions to reply (e.g., “Reply to this email to respond” along with a special reply-to address). The system could use a unique reply-to address or message ID to track the thread (e.g., thread+@ourapp.com).
  8. Teams: The Teams Connector (if using a Bot) sends the message via the bot into a Teams channel or chat. Alternatively, using Microsoft Graph API to post a chat message. The connector could mention specific Teams users if needed (e.g., using @mention tags in the Teams message if someone was mentioned).
  9. Real-Time UI Update: Almost in parallel, the system updates the internal UI clients. The Comment Service (or a related real-time service) notifies the SignalR Hub that a new comment was posted. Using Azure SignalR Service (which can broadcast messages to all connected clients efficiently), it sends a message to all users viewing that thread or relevant page, so they see the new comment in real time without refreshing. This uses the publish-subscribe nature of SignalR groups (each thread could be a group). The event could be triggered by the same CommentCreated event or directly within the Comment Service once the comment is saved. (Azure SignalR Service allows the app to push real-time updates without managing persistent connections, and scales to many clients (Building Real-Time Web Applications Using SignalR).)
  10. External Channels Delivery: Each channel delivers the message to users on that platform. For example, Slack users get a notification in their Slack workspace, WhatsApp users get a message on their phone, etc. Each message as delivered includes context that it originated from the “Comment System” and may include a secure link or instructions to reply directly.
  11. Acknowledgment & Logging: The connectors may log success/failure or handle errors (e.g., Slack channel not found or Twilio message send failure). Retries or dead-lettering can be applied if a channel send fails, but those details are beyond scope. The main flow ends with the comment now visible on all channels and the UI.

B. External Reply Ingestion & Thread Sync: Now consider a user replying from an external channel (say, via WhatsApp):

  1. External Message Event: The user, who saw the comment on WhatsApp, replies “@Alice Sure, I agree with this point.” as a WhatsApp message back to the sender (which is our system’s WhatsApp number). This incoming message triggers a webhook call from the WhatsApp provider. For example, Twilio Programmable Messaging or ACS will send an HTTP POST to a pre-configured endpoint on our system with details of the message (sender number, message text, possibly an ID linking it to the previous message). This is an event originating from an external system and hitting our WhatsApp Connector.
  2. Webhook Receiver (Connector): The WhatsApp Connector (which could be an Azure Function with an HTTP trigger) receives the incoming webhook. It parses the payload to extract the message text and identifies the conversation. If the provider includes a session or conversation identifier, the connector uses that; if not, the system may require the user to include a thread ID in their reply or have only one active thread per user at a time. (Twilio’s Conversations API provides a unified conversation context to manage multi-party threads (Group Texting in Conversations | Twilio), but here we illustrate a custom approach.)
  3. Route to Thread: The Connector calls the Message Router, providing it the incoming message data. The Message Router determines the target thread/comment:
  4. It might look up a mapping by the sender’s phone number and a recent message ID to find the internal thread ID. If the original outgoing WhatsApp message included a hidden token (e.g., “[Thread:12345]”), the reply might quote it or the system might store that association keyed by the user’s identity.
  5. Assuming it resolves to Thread T and parent Comment C (perhaps C was the comment the user replied to), the router prepares a new Comment object with content "Sure, I agree..." with parent_id = C (making it a reply in that thread), and user_id corresponding to the WhatsApp user (the system might map the phone number to a user account; if the replier is an external user not in the system, they could be recorded as an external participant).
  6. Store & Event Emit: The router (or directly the WhatsApp Connector, depending on design) invokes the Comment Service to save this new reply Comment in the database. This is treated like any comment creation – validations run (e.g., mentions parsed, timestamps set) and it’s inserted with a unique ID and parent_id = C. The thread now has a new reply. The Comment Service then emits a CommentCreated (or specifically ReplyCreated) event for this new comment.
  7. Broadcast Reply to Other Channels: The Notification Dispatcher picks up this event just like in flow A. It sees that this comment came from WhatsApp (the event might indicate source channel), and now it needs to broadcast the comment to all other subscribed channels for thread T. For example:
  8. It will send the reply to Slack (so that Slack users see the WhatsApp user’s reply in the Slack thread). The Slack Connector might post this as a message in the same Slack thread (using the thread_ts mapping for thread T) saying “Bob (via WhatsApp): Sure, I agree…”.
  9. It sends to Email (perhaps to all users watching the thread via email, or to specific mentioned users). An email might be sent out: “Bob replied via WhatsApp: ‘Sure, I agree...’”.
  10. It sends to Teams if applicable, and to any other channel except WhatsApp (we avoid sending back to the origin channel to prevent loops). The origin channel (WhatsApp) already has the user's message by definition since they sent it, though in some cases we might send a confirmation or a slightly formatted echo. In Slack’s case, if the origin was Slack, we would skip notifying Slack again or use a bot user vs human logic.
  11. Real-time UI update: The new reply is also broadcast to any web UI clients via SignalR (just as before, so internal users see that Bob’s reply appeared).
  12. Completion: Users on all platforms now see Bob’s reply. From Slack, it appears as if Bob replied (perhaps indicated as coming from WhatsApp user if we add a note or if Bob also has a Slack identity we map). In the web UI, the thread shows Bob’s comment. The thread history is preserved and consistent.

This asynchronous flow ensures minimal latency: the user who replied on WhatsApp gets an immediate delivery through that platform’s normal behavior. The system’s processing (steps 2-5) happens in the background, typically very quickly (a few hundred milliseconds to a few seconds). Thanks to decoupling via the event bus, each part can scale and handle spikes – for instance, multiple replies from various channels can be processed in parallel by multiple function instances or workers (competing consumers on the queue). The producers (channel webhooks) are decoupled from consumers (thread processor) (Event-driven architecture style - Azure Architecture Center | Microsoft Learn), so if there’s a flood of messages, they queue up rather than overwhelm the system, and the system can scale out consumers to catch up. This design also isolates any slow channel (say email sending might be slow) from delaying other parts of the flow.

Mention Notification Process: A brief note on mentions in process: if a comment or reply includes mentions of users, an event (e.g., UserMentioned) can be raised alongside CommentCreated. The Notification Dispatcher can handle this by sending targeted notifications. For example, if Alice mentions @Dave in a comment, the system triggers a notification to Dave’s preferred channel (maybe Dave gets a direct Slack DM or an email saying "Alice mentioned you in [Thread link]"). This uses the same components: e.g., the Slack Connector sending a DM via API, or Email Connector sending an email, with the content of the mention and a secure link to the thread (possibly containing an auth token or a unique URL that requires login to view the thread, thus preserving security). The secure link ensures only the intended user can access the comment, addressing security while using convenience of direct links.

Throughout these processes, the use of an event bus and asynchronous handlers enables high throughput and resilience. Events are delivered to consumers in near real-time and the system can respond immediately as events occur (Event-driven architecture style - Azure Architecture Center | Microsoft Learn). Because producers (e.g., the Comment API or external webhooks) do not need to wait for all notifications to complete, they remain fast. Consumers (dispatchers, connectors) are independent and scalable – we can run multiple instances if needed, and the event bus will distribute load (this follows the competing consumer pattern for parallel processing of events) (Event-driven architecture style - Azure Architecture Center | Microsoft Learn). This is crucial for high volume scenarios (imagine hundreds of comments per second), ensuring the system scales horizontally.

Development View (Implementation & Hexagonal Architecture)

The development view outlines the system’s decomposition into modules, layers, and how it realizes the logical components in code (The 4+1 Architecture Model: A Comprehensive Guide for Software Teams | by Maurice Lichtenberg | Feb, 2025 | Medium) (Understanding Hexagonal Architecture: Ports and Adapters | by Erick Zanetti | Medium). We adopt a Hexagonal Architecture (Ports & Adapters) approach (Understanding Hexagonal Architecture: Ports and Adapters | by Erick Zanetti | Medium) (Understanding Hexagonal Architecture: Ports and Adapters | by Erick Zanetti | Medium) to keep the core domain logic independent of external technologies. This makes the system easier to maintain and extend, as new channels or services can be added by writing new adapters without changing core code.

Core Domain Layer: At the center is the domain model and business logic:

  • We have domain entities like Comment, User, and value objects like Thread (which might not be an explicit table but a conceptual collection of comments with the same root). The Comment entity contains fields and methods relevant to the business (e.g., maybe a method to add a reply or to format content). The Pydantic models shown in the code are part of the API layer, but the domain could use plain Python classes or ORM models for business logic (thread feature description.md) (thread feature description.md).
  • Domain Services: These implement operations like postComment(user, content, mentions, channels) or replyToComment(threadId, user, content), encapsulating the rules (e.g., cannot post empty content, as enforced in the validator (thread feature description.md)). The Thread Manager can be seen as a domain service that handles retrieving full threads or ensuring referential integrity when adding replies.
  • The domain is agnostic of channels and infrastructure. It knows about comments, threads, users, and perhaps events (it might define an event class for CommentCreated). It does not know how to send a Slack message or how to persist to PostgreSQL – those concerns are abstracted behind interfaces.

Ports (Interfaces): Ports define the points of contact between the domain and the outside world (Understanding Hexagonal Architecture: Ports and Adapters | by Erick Zanetti | Medium). We define both inbound (driving) ports for input to the domain and outbound (driven) ports for domain to call external services:

  • Repository Port: An interface for data persistence, e.g., CommentRepository with methods like save(comment) and findThread(threadId) or findById(commentId). The domain code uses this interface to persist and retrieve comments. It doesn’t depend on any specific database tech. In our implementation, an adapter will use SQLAlchemy/PostgreSQL to realize this.
  • Notification Port: An interface in the domain for sending notifications/messages externally. For example, NotificationService or ChannelNotifier interface might have a method like notifyChannels(comment, channels). When a new comment is created, the domain service can call this port’s method. In tests or simple runs, this could be a no-op or in-memory handler; in production, this is implemented by the Notification Dispatcher/Connectors. This port essentially publishes the event. It could be implemented as a simple event emitter (where one adapter pushes to Service Bus) or more abstractly.
  • External Message Port: Conversely, an interface for receiving messages from outside. We might model this as an application service method like ingestExternalReply(channel, externalUser, content, threadRef) which domain logic can handle (validate and turn into a Comment). In a hexagonal sense, the external world (Slack, etc.) calls into our system via an adapter which then calls a domain service. That domain service is an inbound port (also called an application service or use-case in some hexagon interpretations). For example, CommentService.handleIncomingMessage(dto) could be an entry point.
  • Real-time Update Port: An interface the domain can use to push updates to connected clients. This could be abstracted as something like RealtimeUpdater with method broadcastNewComment(comment). In an adapter, this will actually call SignalR to notify clients. By abstracting it, the domain just signals that an update is needed, and the adapter handles the tech detail of SignalR.

Adapters: Adapters implement these ports, separating external detail from core logic (Understanding Hexagonal Architecture: Ports and Adapters | by Erick Zanetti | Medium):

  • Database Adapter (PostgreSQL): Likely our SQLAlchemy integration. For example, a SqlAlchemyCommentRepository implements CommentRepository. When domain calls save(comment), this adapter maps it to an ORM object and commits via SQLAlchemy to PostgreSQL. This adapter depends on the infrastructure (SQLAlchemy, the Postgres connection string, etc.), but the domain doesn’t see those. In FastAPI, this could be wired via dependency injection or just called inside the service implementation. (The provided model code (thread feature description.md) shows how comments are structured; an adapter would use these classes to talk to the DB).
  • Event Bus Adapter: To implement the Notification Port, we could have an adapter that connects to Azure Service Bus or an internal pub-sub. For instance, AzureServiceBusNotifier implements ChannelNotifier.notifyChannels(comment, channels) by constructing a message (e.g., JSON with comment data) and using Azure Service Bus SDK to publish to a topic. This leverages Azure’s reliable pub-sub infrastructure (where subscribers are the Notification Dispatcher functions) (Event-driven architecture style - Azure Architecture Center | Microsoft Learn). Another implementation of this port for local development might just log or directly call the dispatcher method (since hexagonal allows swapping adapters easily).
  • Slack Adapter: This could be split into outbound and inbound parts:
  • Outbound: Implements perhaps a SlackClient interface or uses Slack SDK to send messages. The Notification Dispatcher could call this adapter when dispatching to Slack. But in hexagonal terms, the domain doesn’t call Slack directly; instead, after the event is published, the Slack sending is handled completely by an external service (Notification Dispatcher). If we consider the Notification Dispatcher part of the infrastructure side, then Slack Adapter is an outbound adapter on that side. It takes a domain event or data and converts to Slack API calls.
  • Inbound: The Slack Connector (running as webhook endpoint) is effectively an input adapter – it takes an HTTP request from Slack (JSON event), converts it into a call to a domain service (like MessageRouter.ingest(external_message) or directly creates a Comment via the Comment Service). In FastAPI, this could be a dedicated path like /slack/events handled by a controller that invokes the appropriate domain logic. That controller is an adapter translating HTTP+Slack's format to our internal method call.
  • WhatsApp Adapter: Similarly, an inbound adapter to receive webhooks from Twilio/ACS (could be implemented as an Azure Function or a FastAPI route) and an outbound adapter using the Twilio/ACS SDK to send messages. These adapters encapsulate details like authentication tokens, API endpoints, and message formatting for WhatsApp.
  • Email Adapter: Likely an outbound adapter using an email SDK or SMTP to send emails (for notifications) and an inbound adapter for processing reply emails. The inbound might be trickier if using an email hook service (one might use a function that checks a mailbox or an HTTP endpoint that a service like SendGrid calls with the email contents). In any case, it’s separated from core logic; it will call into domain’s reply handling port.
  • Teams Adapter: Could be implemented via Azure Bot Framework or Graph API. If using Bot Framework, the adapter is essentially the bot’s code that receives messages (inbound) and sends messages (outbound) through the Bot Service. This keeps Teams logic (which might involve Bot SDK, activity handlers, etc.) out of the core domain – the domain just sees messages and sends notifications generically.
  • SignalR Adapter: Implements the Real-time Update Port. For example, uses the Azure SignalR SDK to broadcast to a hub/topic. The domain might call RealtimeUpdater.notifyComment(comment) and the adapter translates that to hubContext.Clients.Group(threadId).SendAsync("NewComment", commentDto) or similar. In FastAPI, this could be done via an async call to the SignalR service REST API or using Azure Functions with SignalR output binding. In any case, it’s at the boundary.

Module Structure: Following the Ports & Adapters style, we can organize the code into layers or modules:

  • domain/ – contains business entities (Comment, User), repository interfaces, service interfaces (ports), and core services like CommentService or ThreadManager.
  • adapters/ – contains sub-folders for each adapter: adapters/db/postgres.py, adapters/messaging/slack.py, adapters/messaging/whatsapp.py, adapters/messaging/email.py, adapters/realtime/signalr.py, etc. Each implements the interfaces from domain. For example, PostgresCommentRepository(CommentRepository), SlackNotifier(ChannelNotifier) etc.
  • application/ or api/ – contains the FastAPI routes and any logic to glue HTTP requests to domain calls (these are input adapters on the driving side). For instance, routes/comments.py defines endpoints and uses CommentService (from domain) to perform actions, then formats responses.
  • workers/ – for asynchronous consumers (could be an Azure Function project or background tasks). These would use the adapters to subscribe to Service Bus and call the dispatch logic. If using FastAPI’s event loop, one might have background tasks for the dispatcher. Alternatively, this is outside of FastAPI as separate function apps or microservices in the deployment (see Physical view). In code, these consumers still utilize domain services or at least adapters to perform their job (e.g., a function that triggers on Service Bus will load the comment data and call Slack adapter, etc.).

This hexagonal design ensures separation of concerns: the domain logic is isolated and can be tested without actual Slack/WhatsApp calls (we can mock the ports) (Understanding Hexagonal Architecture: Ports and Adapters | by Erick Zanetti | Medium) (Understanding Hexagonal Architecture: Ports and Adapters | by Erick Zanetti | Medium). It also means we can replace pieces easily – e.g., switch from Twilio to another WhatsApp API by writing a new adapter, or add a new channel (just add a new connector adapter and plug it into the dispatcher). The code is organized by functionality and adheres to Open/Closed principle – new features (channels) extend via new adapters rather than modifying the core. The overall architecture is more maintainable and resilient to changes in external APIs.

Physical View

The physical view describes how the system is deployed on Azure and the physical infrastructure of each component. The system is designed to be cloud-native, leveraging Azure managed services for scalability, reliability, and ease of integration. The following table outlines the mapping of logical components to Azure services and resources:

Component / Service Azure Implementation Purpose & Notes
FastAPI Application (Comment Service & Thread Manager) Azure App Service (Web App) or Azure Container Apps hosting a Docker container with the FastAPI app. Hosts the core REST API for comments and threads. Manages HTTP requests from the web UI and some incoming webhooks. Scales out to handle API load. Could also be deployed on AKS for more control, but App Service/Container Apps simplifies deployment. Ensure it’s integrated with Azure VNet if needed for security.
PostgreSQL Database Azure Database for PostgreSQL (Flexible Server or Single Server) Stores persistent data: users, comments, threads, mentions. Provides ACID transactions and relational integrity (foreign keys for parent-child comments (thread feature description.md)). Sized and scaled according to expected load (with read replicas if needed).
Real-Time Communication Hub Azure SignalR Service Manages WebSocket connections with clients for real-time updates. Offloads the persistent connection handling from our app. The FastAPI backend or Azure Functions can post messages to SignalR topics to broadcast new comments, which SignalR Service then delivers to connected browsers instantly. This service scales automatically to support a large number of concurrent clients.
Event Bus Azure Service Bus (Topic & Subscriptions) or Azure Event Grid Facilitates the event-driven architecture ([Event-driven architecture style - Azure Architecture Center
Notification Dispatcher Azure Functions (consumption plan) subscribed to Service Bus Topic Implements the asynchronous Notification Dispatcher as a serverless function. For example, one Azure Function triggers on the CommentCreated topic. Within the function, our code (adapter logic) reads the event, then calls the appropriate Channel connectors/SDKs to send notifications. Using Azure Functions here allows automatic scaling: if 100 events come in at once, Azure will spawn more function instances to handle them in parallel, up to throughput limits. This ensures the notification fan-out can keep up with comment volume. Each channel send could even be a separate function (e.g., one function for Slack events, one for WhatsApp) or handled in one function concurrently.
Slack Connector (Outbound) Azure Function (HTTP or Service Bus triggered) or within Notification Dispatcher function Responsible for sending messages to Slack. Could be implemented as part of the Notification Dispatcher (i.e., in the same function) or a separate function/library invoked by dispatcher. It uses Slack’s Web API with the bot token to post messages. No special Azure resource needed beyond having internet access and storing Slack credentials (in Azure Key Vault or App Settings).
Slack Connector (Inbound) Azure Function or FastAPI Route (HTTP Trigger) Exposes an HTTP endpoint for Slack Events API. Slack will send an HTTP POST (event callback) when a user posts a message or reply in channels the app is subscribed to ([Events API
WhatsApp Connector (Outbound) Azure Communication Services (ACS) or Twilio API via Azure Function For sending WhatsApp messages, one approach is using Azure Communication Services which now supports WhatsApp messaging ([Part 1 - Multichannel Notification System with Azure Communication Services and Azure Functions
WhatsApp Connector (Inbound) Azure Function (HTTP Trigger via Webhook) Receives incoming WhatsApp messages. If using ACS, we can integrate via an Event Grid or Webhook for incoming messages (ACS can forward incoming WhatsApp messages to an Event Grid event which a Function can subscribe to). If using Twilio, Twilio will call our Function’s URL with a webhook (Twilio can be configured with a callback URL for incoming messages). This function will parse the message and push it into our system (via Service Bus or direct call to Comment Service). It should run quickly; Twilio expects a fast response (could respond with a 200 and maybe a generic TwiML if needed, but typically just 200 is fine for WhatsApp).
Email Connector (Outbound) Azure Communication Services Email or SendGrid (via Azure Logic App/Function) Sends out email notifications. Azure Communication Services provides Email sending capability (using a verified domain and Email resource) ([Part 1 - Multichannel Notification System with Azure Communication Services and Azure Functions
Email Connector (Inbound) Azure Logic App with Office 365 or SendGrid Inbound Parse to Azure Function Handling email replies is the most infrastructure-specific part. One approach: set up a mailbox (e.g., reply@ourapp.com) and use a Logic App to watch for new emails in that mailbox, then call an HTTP webhook (Function/FastAPI) with the email contents. Another approach: if using SendGrid, use its Inbound Parse Webhook – configure SendGrid to send incoming emails (to a subdomain) to our Function endpoint. The Function then extracts the reply content and original message ID (e.g., from email headers or an ID we embedded in the email) and routes it via the Message Router. This component ensures emails from external email clients can be turned into comment replies.
Teams Connector (Bot) Azure Bot Service (registered Bot with Teams channel) + Azure Function/App Service For Microsoft Teams integration, using a Bot is a standard method. We register a bot (via Azure Bot Service), and add Teams (and potentially Slack) as channels. The bot’s messaging endpoint can be an Azure Function or the same FastAPI app. The Bot Service will relay Teams messages to our endpoint. Outbound, our system can call Teams via the Bot Service SDK or Graph API to post messages. (Alternatively, one could use Graph API webhooks without Bot, but Bot is easier for two-way messaging in Teams). This connector thus might consist of a Bot Framework implementation running on Azure (could even be serverless via Azure Functions using Bot Adaptive Cards etc.). The Teams Connector ensures that a message posted in Teams (say in a channel conversation or via a personal chat with the bot) triggers our logic, and that our notifications appear in Teams chats.
Azure SignalR Binding (Outbound) Azure Function (SignalR output binding) or Serverless Azure SignalR To broadcast from an Azure Function (like the Notification Dispatcher or Comment API), Azure provides a SignalR output binding. For example, the function handling comment creation can have an output binding to SignalR Service which, when the function finishes, sends the message to clients (Building Real-Time Web Applications Using SignalR - LinkedIn). If using FastAPI, we might instead call the SignalR REST API or use an Azure Function as a middle layer. In any case, this is the physical mechanism to deliver real-time updates to web clients.
Secrets & Configuration Azure Key Vault + Managed Identities Stores sensitive credentials: Slack OAuth token, Twilio/ACS keys, SendGrid API key, etc. The App Service and Functions can use Managed Identity to fetch these at runtime, avoiding them being in code. This ensures secure integration with external APIs.
Monitoring & Logging Azure Application Insights and Azure Monitor All components instrument logs and metrics. For example, log when events are published/received, message delivery success or failure per channel. Application Insights can collect telemetry from FastAPI (with OpenCensus), from Functions (built-in integration), and even from the front-end via JS. This gives observability into the system’s operation across the distributed components.

Scalability & High Volume Considerations: Each component in Azure is chosen for scalability. App Service can auto-scale out to multiple instances (the FastAPI app is stateless aside from DB, so it can run multiple replicas). Azure Functions on a consumption plan automatically scale to handle event volume (within limits) – e.g., the Service Bus triggered functions will scale out when the queue length grows. Azure Service Bus itself can handle a very high throughput of messages (topics can process thousands of messages per second) – acting as a buffer so that spikes in comments or replies don’t overwhelm downstream services. Azure SignalR Service can scale to support a very large number of concurrent connections and messages by scaling units. We also considered using Azure Container Apps for the connectors; Container Apps can run background processors and scale based on queue length or events similarly, but Functions provide built-in triggers for Service Bus and webhooks which simplify the implementation. The database (Azure PostgreSQL) can be scaled up (more vCores, memory) or scaled out for reads if necessary; careful indexing (on parent_id, etc.) will be done to optimize thread queries. Caching layers (like Azure Cache for Redis) could be introduced in front of the DB for frequently read data (e.g., caching popular threads or user data) if performance demands.

Networking & Security: All service-to-service calls within Azure use secure connections (Service Bus, SignalR, etc., all over TLS). External channel calls (Slack, Twilio, etc.) go over HTTPS. We secure webhooks (like Slack, Twilio) by validating tokens or signatures these platforms send (to ensure the request is genuinely from Slack, etc.). The FastAPI and Function endpoints can be protected behind an API gateway or use Azure APIM if needed, but since Slack/Twilio can include their own auth, we mainly ensure endpoints are only known to those services. The user-facing UI likely sits behind Azure Front Door or is a static web app calling our API with authentication (e.g., using OAuth or JWTs for users). The “secure links” sent in notifications are signed URLs or contain unique tokens that map to the user and comment, which the Comment Service verifies before showing content (or the link directs the user to login if not already authenticated). This ensures that if an email is forwarded, unauthorized users cannot access the thread.

In summary, the physical deployment on Azure uses a mix of PaaS and serverless components: App Service for the core API, Azure Functions for event handling and integration endpoints, Service Bus for messaging, Azure SignalR for realtime, Azure Bot Service for Teams, and managed APIs (ACS/Twilio) for external channels. This architecture is designed to be cloud-scalable and resilient, with no single point of failure (multiple instances and queue-based load leveling) and with managed services handling the heavy lifting of scale.

Scenarios (4+1 Use-Case View)

Finally, the scenarios view ties the architecture together by walking through representative use-cases (The 4+1 Architecture Model: A Comprehensive Guide for Software Teams | by Maurice Lichtenberg | Feb, 2025 | Medium) (The 4+1 Architecture Model: A Comprehensive Guide for Software Teams | by Maurice Lichtenberg | Feb, 2025 | Medium). Each scenario demonstrates how the components collaborate to fulfill requirements:

1. Posting a Comment with Multi-Channel Notifications

Alice, an application user, submits a new comment in the app and chooses to notify the Project Chat channel in Slack and also send it as an email to an external partner. Upon submission, the Comment Service saves Alice’s comment (e.g., “We have updated the design docs.”) and publishes a CommentCreated event. The Notification Dispatcher picks it up and sees Slack and Email are targets. The Slack Connector posts Alice’s message to the #project-chat Slack channel, and the Email Connector sends out an email to the partner (with subject “New comment from Alice”). Alice’s comment ID and thread info were embedded: the Slack message includes a thread reference so replies in Slack stay threaded, and the email’s reply-to is set to a special address linked to the comment ID. Alice immediately sees her comment in the web UI; her colleague Bob sees a Slack notification in #project-chat with Alice’s message, and the external partner gets an email in their inbox. Additionally, any users currently viewing the thread in the web app see Alice’s comment pop up in real-time via SignalR. This scenario shows one-to-many delivery of a comment across channels in near real-time, using the event-driven fan-out and channel adapters.

2. Replying via WhatsApp and Syncing Back to Thread

Bob, who saw Alice’s comment in Slack, is traveling and has notifications also set up on WhatsApp. The system sent Alice’s comment to Bob’s WhatsApp (via Twilio/ACS) as well, because Bob subscribed to WhatsApp alerts for this project. Bob receives a WhatsApp message: “Alice (via Project App): We have updated the design docs.” with perhaps a link. Bob replies directly in WhatsApp: “Great, I’ll review by EOD.” This incoming message hits our WhatsApp Connector through a webhook. The system identifies Bob (from his phone number) and the thread (via a token in the message or Twilio conversation context). The Message Router creates a new Comment in the thread for Bob’s reply (“Great, I’ll review by EOD.”) with parent = Alice’s comment. The Thread Manager records it and triggers the usual notification flow. Now the Notification Dispatcher broadcasts Bob’s reply to all relevant channels: Alice (the thread owner) gets notified. In Slack’s #project-chat, Bob’s reply appears as a threaded response (the Slack Connector posts “Bob (from WhatsApp): Great, I’ll review by EOD.” in the thread). Alice, if she’s online in the web UI, sees Bob’s reply in real time. The external partner who was on email might also get an email update (if they are subscribed to thread updates, they receive an email: “Bob replied via WhatsApp: ‘Great, I’ll review by EOD’”). The WhatsApp Connector does not echo the message back to WhatsApp since Bob is the one who sent it (though a confirmation could be sent if desired). Now all participants – Slack, WhatsApp, Email, and the app UI – have the latest state of the conversation. Bob’s WhatsApp reply is seamlessly integrated; to Alice and others it feels like Bob responded to the thread, regardless of medium. This scenario highlights cross-channel two-way integration, where a message from one channel propagates to others, keeping the thread consistent.

Carol is another user who was not actively following the thread. Alice, in a subsequent comment, writes “I think we should involve @Carol for feedback.” and posts this comment. The system recognizes that Alice mentioned Carol (the Comment object’s mentions list will include Carol’s user ID (thread feature description.md)). As part of processing this comment, a UserMentioned event for Carol is generated. The Notification Dispatcher (or a dedicated Mention Service) handles this by notifying Carol on her preferred channel. Suppose Carol’s preferences indicate she wants Microsoft Teams notifications for mentions. The Teams Connector sends a proactive message to Carol via the bot: “Alice mentioned you in Project Design thread: I think we should involve @Carol...” with a button or link “View Comment”. This link is a secure deep link that opens the web application directly to the thread (after authenticating Carol). Because Carol was not previously a participant, she also receives context: the Teams message might include a summary or the latest few messages in the thread for context. When Carol clicks the link, she’s taken to the web app’s thread view (the link contains a token identifying the thread and possibly the mention event, so the app knows to highlight the mentioned comment). Meanwhile, Bob (on Slack) and the external partner (on email) also see Alice’s comment that included Carol’s name. If Carol instead had email as her notify channel, she would get an email saying “Alice mentioned you...” with the link. The mention notification ensures Carol is aware and can join the conversation. When Carol replies (say she does so from Teams by messaging the bot “Sure, I’ll take a look.”), the process is similar to Slack/WhatsApp: the Teams Connector receives the message and the router creates a comment, broadcasting it to Slack, WhatsApp, Email, and back to Teams (if needed for other Team members). Secure links in these notifications use tokens or user-specific URLs so that only Carol can use that link (the app verifies her identity or the token corresponds to Carol’s account, preventing misuse).

In all these scenarios, the thread state remains consistent across platforms. Every comment and reply ends up in the central database and is redistributed, so participants on any channel see the full conversation history. The system supports a rich set of requirements: real-time internal updates, asynchronous external delivery, mention awareness, and security considerations for links and data. The use-case narratives also demonstrate the scalability and reliability of the design:

  • Even if many users post and reply concurrently, the event-driven architecture with Azure Service Bus and Functions can scale to handle the load, queueing events if necessary and processing them as resources permit.
  • If one channel is down or slow (imagine email service latency), it doesn’t block the others; each connector works independently (perhaps with its own retry policies or dead-letter queues for failures).
  • The design is extensible: adding another channel (say, SMS) would mean adding an SMS Connector and plugging it into the Notification Dispatcher (ACS could even handle SMS using the same mechanisms as WhatsApp in this case).

Conclusion: This 4+1 architecture provides a robust blueprint for extending the comment/thread system into a multi-channel communication hub. The logical view outlined the key components like Comment Service, Notification Dispatcher, Channel Connectors, etc., and their roles in the system. The process view illustrated how events flow asynchronously to synchronize threads in real-time across diverse channels. The development view showed how a hexagonal architecture ensures a clean separation of core logic from channel-specific details, easing maintenance and future growth (Understanding Hexagonal Architecture: Ports and Adapters | by Erick Zanetti | Medium) (Understanding Hexagonal Architecture: Ports and Adapters | by Erick Zanetti | Medium). The physical view mapped these ideas onto scalable Azure services (App Service, Functions, Service Bus, SignalR, PostgreSQL, ACS, etc.), emphasizing high-volume handling and reliability. Finally, the scenarios demonstrated the system in action for typical use cases: broadcasting a new comment, integrating an external reply, and handling user mentions with notifications. By following this architecture, the system will enable users like Alice, Bob, Carol, and external partners to collaborate through their preferred communication channels while maintaining a single coherent conversation thread, thereby greatly enhancing engagement and productivity across platforms.