AI Does Not Belong to the Core: Modular Architecture with an AI Layer on the Threeger Platform
When I started designing the architecture of the Threeger Platform, one decision separated me from a sustainable product and a set of fragile integrations disguised as a platform: where exactly should AI fit into the system?

The wrong answer is simple and tempting. You add an OpenAI client inside the ConversationService, call the model when a message arrives, and in two days you have a working prototype that impresses any demo. The problem is that this prototype is already born with architectural debt. Not technical debt in the sense of bad code. Structural debt, in the sense that the business rule has come to depend on an infrastructure detail that will change.
Providers change. Models are discontinued. Prices go up. Rate limits appear. And when the core of the product is already tightly coupled to these details, each change requires surgery on the heart of the platform.
This article explains how I designed the architecture of the Threeger Platform to avoid exactly that.
What the core needs to protect
The Threeger Platform operates in the space of CRM and multichannel service automation: WhatsApp, Instagram, Telegram, email, web widget, and API. The operational core of the product deals with conversations, contacts, inboxes, teams, pipelines, journeys, automations, campaigns, reports, billing, and auditing.
None of these business concepts depend on which language model is running in production. A conversation does not need to know if the response was generated by GPT-4o, by Claude Sonnet, or by an internally fine-tuned model. An automation pipeline does not need to know the response schema of the Qdrant API.
The core must protect this separation.
This is not an architectural opinion. It is a direct consequence of the dependency inversion principle applied at the domain level. The domain does not depend on infrastructure. The AI infrastructure is implemented to serve the domain, not the other way around.
In practice, this means that the ConversationService never calls OpenAI. It calls an LLMPort. Whoever implements this port can be OpenAI today, Claude tomorrow, or a local model next week. The business rule does not need to be touched at any of these times.
The same applies to semantic search. The core does not know Qdrant or Pinecone. It knows VectorSearchPort. Embeddings? EmbeddingPort. Intent classification? ClassificationPort. Moderation? ModerationPort. Automatic conversation summarization? SummarizationPort. Access to the centralized AI gateway? AIGatewayPort. Each capability has its contract. None has its implementation leaking into the domain.
The Channel Adapter Layer as a normalization boundary
Before any domain event exists, there is a more immediate problem: each input channel speaks a different language.
A message from WhatsApp arrives via webhook with a specific payload from Meta. A message from Instagram has a different structure, with its own fields for stories, reactions, and DMs. The web widget sends events via WebSocket. The public API receives REST calls with contracts defined by the client. Email has headers, threading, and multipart.
Putting this diversity directly into the Application Layer is a mistake that accumulates quickly. The input code becomes a maze of conditionals by channel, each with its special handling, its exceptions, and its normalization logic mixed with business logic.
The Channel Adapter Layer resolves this before the problem reaches the core. Each channel has its dedicated adapter, responsible for one thing only: transforming the specific input of the channel into a normalized domain event. The WhatsAppAdapter knows what a wamid is. The InstagramAdapter knows what a mid is. The EmailAdapter knows how to parse MIME threading. The core knows none of this and does not need to know.
When a new channel is added to the platform, the integration work is contained in the new adapter. No business rule is altered. No existing domain event needs to change. The new channel simply learns to speak the core's language.
Above the Channel Adapter Layer is the BFF/API Gateway, responsible for authentication, session management, permission control, and rate limiting. A request that reaches the core has already gone through channel normalization and access control. The domain receives clean context, never raw payload.
Why events are the right glue for this type of system
The Threeger Platform deals with many simultaneous flows. A message received via WhatsApp triggers a sequence that may involve updating a conversation, executing automation, fetching context, generating a response, validating by guardrail, notifying the attendant, and recording metrics. This does not account for active campaigns, external webhooks, and manual operator actions happening at the same time.
A linear and synchronous design does not scale in this scenario. More than that, it forces an artificial coupling between operations that are conceptually independent.
The solution is to model the system around domain events. When a message arrives, the core processes what is its responsibility and publishes message.received. What happens next is the responsibility of whoever subscribes to that event. The AI layer subscribes. The automation module subscribes. The analytics module subscribes. Each processes independently, without the core needing to orchestrate this chain.
Relevant events in the platform include conversation.created, contact.updated, automation.triggered, campaign.started, ai.intent.detected, and conversation.assigned. Each carries enough context for the consumers to act without needing to make additional queries to the core.
This separation has a concrete benefit that goes beyond scalability: traceability. Each event is an auditable record of what happened in the system. When something goes wrong in production, I do not need to reconstruct the state from stack trace logs. The event log tells the story.
How the AI layer fits outside the core
The AI layer of the platform is not a monolithic service. It is a set of capabilities that the core can access via well-defined contracts.
Within this layer, there are components with distinct responsibilities. The AIGatewayPort centralizes routing to external providers and manages authentication, retry, and fallback between models. The agents execute more complex reasoning flows when necessary. The RAG pipeline retrieves relevant context from the client's knowledge base before a response is generated. Intent classification categorizes received messages to guide automatic routing. The SummarizationPort generates automatic summaries of conversations, useful for both handoff between attendants and operational reports and compressed context in long conversations. The guardrails module validates responses before they reach the attendant or the end customer. And AI observability records latency, cost per operation, embedding quality, and fallback rate.
What unites these components is that none of them knows the business rules of the platform. They receive context via event or port call, execute their specific function, and return a result. The decision on what to do with that result is up to the core.
This matters more than it seems. When the guardrail rejects a generated response, who decides if that means escalating to a human attendant, trying again with a different prompt, or logging it as a failure is the core, applying the policy configured by the client. The AI detects. The domain decides.
The complete flow in production
In a production environment, a message received via any channel follows a path that makes this separation of responsibilities clear.
The channel delivers the message to its dedicated adapter in the Channel Adapter Layer. The adapter normalizes the payload, converts the specific channel format into a standardized domain event, and forwards it through the BFF, where authentication and rate limiting are applied. The message.received event is published on the bus. The conversation worker consumes the event, applies the business rules (is there an open conversation for this contact? is the contact in any automation flow? is there a configured service time?), updates the state, and publishes conversation.updated.
The AI worker consumes this event and initiates the intelligent processing pipeline. First, it fetches context from the RAG using the conversation history and the contact profile as a query. If the conversation is long, the SummarizationPort compresses the history before it is sent to the model, reducing token cost without losing relevant context. With context available, it classifies the intent of the message, triggers the agent responsible for generating the suggested response, and passes through the guardrail before returning. The entire operation is recorded with latency, model used, estimated cost, and result.
The attendant or the automation flow receives the suggestion and decides what to do with it. The AI does not act. It supports.
This model has a higher setup cost than a quick prototype. But in production, with multiple clients, simultaneous flows, and the need to switch providers without downtime, it pays the bill with margin.
What changes when you need to switch providers
This is the real test of the architecture.
Suppose the cost of OpenAI rises by 40% and an alternative model with equivalent performance becomes available. In a tightly coupled system, this means finding all the calls to the OpenAI SDK scattered throughout the code, understanding the context of each one, rewriting the clients, testing each integration point individually.
In the Threeger Platform, the process is different. I create a new adapter that implements LLMPort pointing to the new provider. I configure the AI Gateway to route to the new adapter. I run the port contract tests. If they pass, the deployment is ready.
The ConversationService has not been touched. The AutomationService has not been touched. No business rule has been altered. Because no business rule depended on the provider.
The same reasoning applies to vector databases, embedding models, moderation services, classification tools, and summarization models. Each has its port. Each port has its adapters. The adapters are the only part of the system that knows the external details. The AIGatewayPort, in particular, centralizes this control for LLM providers, allowing routing different types of tasks to different models without any layer above the gateway needing to be altered.
Observability is not optional in production with AI
One mistake I often see in products that incorporate AI is treating observability as an afterthought. Application logs record that AI was called. The result appears in the conversation. What happened in between remains invisible.
In a real production environment, invisibility has a cost. When the rate of inadequate responses increases, I need to know if the problem lies in the RAG retrieving the wrong context, in the prompt generating an out-of-spec response, in the guardrail with a poorly calibrated threshold, or in the model showing quality degradation.
The AI layer of the platform records each operation with enough context for this type of diagnosis. Latency by component, document retrieved by the RAG and its similarity score, version of the prompt used, model and temperature of the call, result of the guardrail with the reason for rejection when applicable, and estimated cost of the operation.
This data feeds operational dashboards that allow identifying degradation patterns before they reach customer complaints. It is not conventional application monitoring. It is observability of AI system behavior in production, which is a different discipline.
The principle that guides all these decisions
AI providers, models, vector databases, agent frameworks, and orchestration tools will continue to evolve at a rapid pace. What is state-of-the-art today has a real probability of becoming legacy in eighteen months.
The business rule of the Threeger Platform, what defines how a conversation is managed, how a contact is organized, how an automation is executed, how a team operates, this part needs to survive these changes without needing to be rewritten every time.
The separation between core and AI layer is not an aesthetic architectural preference. It is what ensures that the product can evolve its intelligence without compromising its operational stability.
Building with AI is not hard. Building a product that uses AI and is still reliable, auditable, maintainable, and capable of evolving, that requires architecture.
Alexsander Valente is a software engineer and architect with 15 years of experience in production systems. He is the founder of Threeger and a professor of conversational agent development in the MBA program at Ibmec.


