Charlie: A simple and practical architecture for a WhatsApp Bot
Many of us are fascinated by the potential of AI in real-world applications. Following my previous exploration of AI assistants with Whisper and GPT-3.5, I decided to tackle a more fundamental challenge: building a simple foundation for WhatsApp bots.
Far from being a smart assistant highly integrated with large language models, Charlie was born out of sheer curiosity: how do you actually build a bot for WhatsApp? The goal wasn’t to create a truly “intelligent” assistant, but rather to design a foundation that could evolve with high availability, scalability, and full control over system behavior. This article presents Charlie’s microservice architecture as a case study for developers interested in building similar projects.
The complete implementation lives in the open-source repository.
Why not use low-code tools?
While tools like n8n excel at rapid prototyping, they often fall short when it comes to customization, performance optimization, and truly understanding architectural nuances. Charlie was conceived as a hands-on learning experience, a quick dive into message queues, decoupled services, and distributed deployments. These concepts are best grasped through coding.
Architecture: decoupling and resilience
Communication with WhatsApp is handled via the Evolution API. To ensure scalability and resilience, Charlie’s architecture is based on microservices interconnected through a message broker. Let’s break down the key components:
- Evolution API: acts as our bidirectional interface with WhatsApp, receiving and sending messages.
- Enqueuer Webhook: responsible for intercepting incoming messages from the Evolution API. It parses the payload and immediately queues the message into the Incoming Messages Queue (IMQ), ensuring decoupled reception and processing.
- RabbitMQ Instance: the heart of asynchronous communication.
- Incoming Messages Queue (IMQ — FANOUT): this queue receives all raw messages from the Enqueuer. The FANOUT type allows multiple consumers to listen to the same message. In the current setup, the Intent Parser Service is the main consumer, but this adds flexibility for future components (e.g., logging, auditing).
- Outgoing Messages Queue (OMQ): bot-generated responses are placed in this queue before being dispatched to users.
- Intent Parser Service: the brain of Charlie. This service consumes messages from the IMQ and performs Natural Language Processing (NLP) to detect user intent. It currently supports Greetings, Daily Fortune, a Bible Verse and Gym Motivation. Once the intent is identified and the response generated, it is sent to the OMQ.
- Dispatcher Service: the final executor. It consumes messages from the OMQ and sends them back to the user via the Evolution API.
Bonus: few-shot intent classifier
Under the intent parser service, you will find a notebook for training the intent classifier using a SentenceTransformer. The approach consisted of fine-tuning the pre-trained sentence-transformers/paraphrase-MiniLM-L6-v2 model using a small set of training examples (just 50 samples across 5 intent classes) — a technique known as few-shot learning. With this small dataset, the model is able to detect user intent based on the semantic similarity of phrases, leveraging the power of embedding representations.
Benefits of a microservice architecture
The decision to use microservices and message queues is far from arbitrary. It addresses key challenges in distributed systems:
- High Availability: a failure in one service does not bring down the entire system; messages persist in queues until the service recovers.
- Horizontal Scalability: each service can scale independently, optimizing resource usage.
- Decoupling: autonomous services simplify development, maintenance, and deployment, enabling independent teams.
- Resilience: queues act as buffers, absorbing traffic spikes and shielding services from overload.
This architecture can be easily extended into a more sophisticated structure, incorporating load balancers, partitioning, and adapting seamlessly to your specific requirements.
The advantages of a monorepo
Although Charlie’s architecture is microservice-based, it’s organized as a monorepo. For a learning project or a foundational prototype like this, a monorepo offers several advantages:
- Simplified Development: all services reside in the same repository, making it easier to navigate, refactor, and share code.
- Dependency Management: easier management of versions and shared dependencies.
- Holistic Visibility: provides a clear view of the entire system.
The monorepo vs. polyrepo debate is beyond the scope of this post. But here’s the idea: create a branch for each service and follow your preferred Git flow. Once everything is done, tested, and well validated, open a pull request to the main monorepo branch. Simple.
A starting point for a WhatsApp bot
Charlie fulfilled its purpose as a learning environment and prototyping ground for microservices and messaging API integration, with a touch of machine learning through the intent classifier. Although it’s a “toy project,” the architecture and concepts implemented here serve as a foundation for building scalable and resilient WhatsApp bots.
I invite you to explore the Charlie repository on GitHub and use it as inspiration. I hope you’ve enjoyed this article — and may the code be with you!