Architecture - IIIA-KO/Lanka GitHub Wiki
Lanka’s architecture is a modular monolith structured around Domain-Driven Design principles. The system is divided into discrete modules, each encapsulating a specific set of business capabilities and domain logic. This modular approach keeps the codebase organized and allows each domain area to evolve independently, while still running as a single deployable application.
Core Modules (Domains):
-
Users Module: Manages user accounts, registration, and authentication. It integrates with Keycloak for secure identity management (delegating tasks like login and token issuance to Keycloak). User-related domain events such as UserCreated or InstagramLinked are produced here.
-
Analytics Module: Handles collecting and processing Instagram statistics and other analytical data. This module would interface with external APIs (e.g., Instagram’s API) to fetch insights. It triggers events like DataIngested after processing new data.
-
Matching (Search) Module: Implements smart matching algorithms to connect influencers (bloggers) with advertisers. Using data from Analytics and User profiles, it generates match events (e.g., MatchRequested, MatchFound) that indicate potential collaborations.
-
Campaigns Module: Manages advertising campaigns and their lifecycle. This includes creating campaign offers, establishing agreements (pacts) between advertisers and influencers, and tracking campaign status. Campaigns go through statuses such as Pending, Confirmed, Rejected, Done, Completed, etc., with corresponding domain events like CampaignCreated, CampaignUpdated, or CampaignCancelled. The Offers & Pacts sub-domain ensures that when a user registers, a “Blogger” profile is created and that offers from advertisers can be accepted or rejected by influencers, resulting in a pact (agreement).
-
Communications Module: Responsible for notifications and communication between users (for example, sending out notifications when an offer is made or a campaign status changes). This module heavily uses the Outbox pattern – domain events from other modules (like a campaign status change or new match found) are recorded and later dispatched as notifications or emails. It ensures that messages/notifications are sent reliably and only once.
Architecture Style:
The backend uses a CQRS (Command Query Responsibility Segregation) pattern for processing client requests. This means write operations (commands) are handled separately from read operations (queries), often with different models optimized for each. For instance, creating a campaign (command) and retrieving a list of campaigns (query) are distinct concerns. This separation, combined with domain-driven design, improves scalability and maintainability. Commands result in state changes and raise domain events, whereas queries fetch data (often via dedicated read models or projections).
Inter-Module Communication:
Even though Lanka is a single application, modules communicate with each other in a loosely coupled, event-driven fashion. When something of importance happens in one module (e.g., a new campaign is created in the Campaigns module or a user links an Instagram account in the Users module), a Domain Event is raised within that module. Domain events represent internal happenings (e.g., UserRegisteredEvent
, CampaignCreatedEvent
) and are handled inside the module. If other modules need to know about this event (for cross-cutting side effects or data sync), the domain event is translated into an Integration Event. Integration events are messages that are published to an Event Bus, which other modules subscribe to, enabling cross-module workflows.
Lanka utilizes MassTransit (a .NET messaging library) with RabbitMQ as the underlying message broker for its event bus. MassTransit allows definition of message contracts (events) and consumers for those events. It abstracts the low-level RabbitMQ details, automatically creating exchanges and queues for each event type and consumer. In Lanka:
- Events (integration events) are published to the broker (RabbitMQ) via MassTransit when something noteworthy occurs.
- Other modules have consumers listening on relevant queues; when they receive an event, they handle it (e.g., the Communications module might listen for a CampaignConfirmed event to send a notification).
To ensure this asynchronous communication is reliable and idempotent, Lanka implements the Outbox and Inbox patterns for messaging.
When a module wants to publish an integration event, it doesn’t send it to RabbitMQ immediately. Instead, it stores the event in an Outbox table as part of the same database transaction as the business operation. A background process (e.g., a Quartz.NET job) periodically reads the Outbox table and publishes those events to RabbitMQ, marking them as sent. This guarantees that if the business operation succeeds, the event will eventually be delivered (at-least-once delivery), and if the operation is rolled back, no event is sent.
Conversely, when consuming events, the consumer module first records each incoming message in an Inbox table before processing it. The event handler then checks the Inbox to ensure it hasn’t seen this message ID before (to avoid duplicates) and processes it, marking it as processed in the Inbox. This achieves exactly-once processing of integration events across modules. In summary, the Outbox/Inbox patterns together reinforce at-least-once delivery and exactly-once processing in the distributed messaging within the monolith, preventing lost messages and duplicate handling.
Web API and Gateway:
The system exposes its functionality via a set of HTTP APIs. There are two main entry points:
- Lanka.API: The primary web application (an ASP.NET Core API) that hosts endpoints middlewares. It contains the global exception handling and unites each module’s commands and queries. This API is secured (Keycloak is used to protect endpoints with OAuth2/OpenID Connect, so that only authenticated users can access certain routes).
- Lanka.Gateway: A gateway service that sits in front of the API. Implemented using YARP (Yet Another Reverse Proxy), the gateway forwards external HTTP requests to the appropriate API endpoints. The gateway is also configured with Polly policies for resilience and Rate Limiting to protect the backend from overload. In practice, the Gateway handles concerns like:
- Routing: Using YARP to map incoming paths (e.g., /api/users/*) to the Lanka.API.
- Resilience: Using Polly to automatically retry transient failures, use circuit breakers, and enforce timeouts when calling the API.
- Throttling: Applying rate-limiting rules (e.g., limiting requests per IP) to prevent abuse of the API.
By having a gateway, the architecture can evolve towards microservices in the future (the gateway could route to different services per module), but currently it fronts a modular monolith. It also allows adding cross-cutting concerns (like logging, metrics, auth) in one place. For authentication, the gateway can validate JWT tokens issued by Keycloak before forwarding requests to the API, ensuring only authorized calls reach the core application.
Data Persistence:
All modules share a common PostgreSQL database (for now) with schema separation by module or simply distinct tables for each context. The project likely uses Entity Framework Core as the ORM (implied by the mention of an EF Core interceptor for capturing domain events in the Outbox).
The Unit of Work pattern is utilized via EF Core’s transaction management so that each business operation either fully succeeds or fails atomically.
Aggregates are the primary consistency boundary – e.g., a Campaign aggregate and its related Offers will be manipulated in a single transaction, then the Outbox event for CampaignCreated is saved.
Telemetry and Observability:
Observability is a first-class concern in Lanka. The project integrates OpenTelemetry for distributed tracing and metrics, Serilog for structured logging, and exports data to monitoring tools:
- Traces and spans (for monitoring request flows and performance) are collected via OpenTelemetry and sent to Jaeger for visualization.
- Structured logs (with contextual information like Trace IDs for correlation) are produced by Serilog and shipped to Seq, a centralized log server.
- Application metrics (like request rates, error counts, etc.) can be gathered via OpenTelemetry exporters or additional libraries and viewed in dashboards (the stack could be extended to Prometheus/Grafana if needed).
All services (API, Gateway, background jobs) are instrumented so that any request can be traced end-to-end. For example, a request coming through the Gateway into the API will carry a trace ID; any subsequent message publication to RabbitMQ will propagate that context so that when a consumer processes the event, the trace can continue.
This gives developers insight into the behavior and performance of the system in real time. It helps identify bottlenecks or failures (for instance, if a message failed to be processed and went to a dead-letter queue, there would be logs and traces for that).
Overall, Lanka’s architecture emphasizes clear domain boundaries, reliable asynchronous communication, and scalability. While currently implemented as a single deployable (monolith), the practices in place (CQRS, event bus, gateway, etc.) lay a path toward a microservices or distributed system in the future if needed. The design decisions are recorded in the Architecture Decision Log within docs/architecture-decision-log, which explains the rationale behind choosing certain patterns (for instance, adopting Outbox/Inbox for reliability, or using a gateway) and the consequences of those decisions