Monolith, Microservices, Event-Driven Architecture, and Model Registry
Prerequisite Knowledge
This lecture builds on the following concepts from earlier lectures. If any feel unfamiliar, review the linked notes before proceeding.
Previously Covered in This Subject
- CQRS (Command Query Responsibility Segregation) — covered in Lecture 5
- Retrieval-Augmented Generation (RAJ) — covered in Lecture 5
- Pipe and Filter Pattern — covered in Lecture 5
- ML Pipeline — covered in Lecture 2
- Architectural Patterns — covered in Lecture 4
Monolith, Microservices, Event-Driven Architecture, and Model Registry
6.1 RAJ Architecture with Multimodal Input
6.1.1 Multimodal Input Strategies
What happens when your documents contain images, not just text? Lecture slides, medical scans, engineering diagrams, and handwritten notes all carry information that pure text pipelines cannot reach. How do we feed these into a Retrieval-Augmented Job (RAJ) system that was designed around text chunks and embeddings?
In the previous session, the professor introduced the RAJ architecture: load documents, split them into chunks, embed those chunks, store them in a vector database, retrieve the most relevant chunks at query time, and let an LLM generate a grounded answer. That pipeline works cleanly when the input is plain text — PDFs, Word files, CSVs. But real-world documents are rarely pure text. Lecture slide decks mix bullet points with diagrams. Medical reports embed X-rays alongside narrative descriptions. Research papers scatter figures throughout their body. The question becomes: how does the RAJ pattern adapt when the input is an image or a document that contains images?
The professor outlined two concrete strategies, and the key insight is that both strategies converge to the same downstream pipeline — only the input pre-processing stage changes.
Approach 1 — OCR then text pipeline
OCR (optical character recognition) is a technology that reads text from images — think of it as a scanner that converts a photograph of a page into editable text. Tools like Tesseract (open-source, maintained by Google) or cloud services like AWS Textract perform this extraction step.
The workflow is straightforward:
- Extract text from each image using OCR.
- Feed the extracted text into the standard RAJ pipeline: chunk, embed, store in ChromaDB, retrieve at query time.
This approach treats images as a text-extraction problem. Once the text is recovered, the rest of the RAJ architecture does not need to know the original source was an image. It is the simpler approach, and it works well when the images primarily contain text (typed documents, printed slides, handwritten notes with clear handwriting).
Scope: OCR works best for text-heavy images. It degrades significantly on complex diagrams, charts, photographs, or handwritten content with poor legibility. If your document set contains many non-text images, OCR alone will lose most of the information.
Approach 2 — Multimodal LLM directly
Modern LLMs are multimodal — they can process both text and images natively. Models like GPT-4o, Gemini, and Claude can look at an image and describe its content in natural language. Instead of running OCR, you pass the image directly to a multimodal LLM and ask it to produce a textual summary (typically 100–200 words).
The workflow becomes:
- Send the image to a multimodal LLM with a prompt like "Describe the content of this image in 100–200 words."
- Receive a textual summary that captures not just any text in the image, but also the visual content — shapes, relationships, labels, layouts.
- Feed that summary into the standard RAJ pipeline: chunk, embed, store, retrieve.
The LLM understands the image content and produces a concise text representation. This summary then enters the vector store just like any other text chunk.
Worked example — a lecture slide with a diagram:
Suppose a lecture slide contains a flowchart showing "User Query → Embedding Model → Vector Store → LLM → Response" with arrows connecting each box.
- OCR approach: Extracts only the text labels ("User Query", "Embedding Model", "Vector Store", "LLM", "Response") and misses the arrow connections and layout that show the flow direction.
- Multimodal LLM approach: Produces a summary like "This diagram shows the RAJ pipeline flow: a user query is first converted to an embedding, then matched against a vector store, and the retrieved context is passed to an LLM which generates the final response. Arrows indicate the sequential flow from left to right."
The multimodal approach preserves the structural and relational information that OCR cannot capture.
Why both approaches are viable today
Multimodal LLMs have matured significantly. A few years ago, only specialized vision models could interpret images, and they required fine-tuning. Today, general-purpose LLMs handle images out of the box with no additional setup. This makes Approach 2 practical for most use cases.
The critical architectural insight — the one the professor emphasized — is that the RAJ pattern itself does not change. Both approaches ultimately produce text that gets chunked, embedded, and stored in the vector database. The retrieval and generation stages are identical regardless of whether the input started as plain text, OCR-extracted text, or an LLM-generated image summary. This is the power of the RAJ design: the core pipeline is stable, and only the ingestion adapter changes.
Core principle: The RAJ architecture is input-agnostic once data reaches the chunking stage. Images, tables, audio transcripts, or any other modality can be supported by swapping the pre-processing adapter while keeping the rest of the pipeline unchanged. This is an example of the adapter pattern — a standard software design pattern where a wrapper converts one interface into another.
Practical considerations when choosing between the two
| Factor | OCR + Text Pipeline | Multimodal LLM |
|---|---|---|
| Cost | Lower (OCR is cheap or free) | Higher (LLM API calls per image) |
| Speed | Faster (OCR is lightweight) | Slower (LLM inference per image) |
| Text-heavy images | Works well | Works well |
| Diagrams, charts, photos | Loses visual information | Captures visual content |
| Setup complexity | Simple (Tesseract install or cloud API) | Simple (LLM API key) |
| Accuracy on clear text | High | High |
In practice, many production systems use a hybrid approach: run OCR first for text-heavy pages, and fall back to the multimodal LLM for pages where OCR returns sparse or low-confidence results. This balances cost against information capture.
Common pitfall: Assuming that OCR output quality matches the original document. OCR introduces errors — misread characters, broken formatting, lost table structure. Always validate OCR output on a sample before trusting it for a production RAJ system. For critical applications (legal, medical), the multimodal LLM approach is often more reliable because it interprets the image holistically rather than character-by-character.
Real-world connection. Companies processing large document corpora — insurance claims with attached photos, legal contracts with embedded diagrams, academic papers with figures — increasingly use multimodal LLMs as their primary ingestion strategy. The cost premium over OCR is offset by the significantly richer information capture, especially when downstream accuracy matters.
6.2 Implementing RAJ with CQRS and Pipe-and-Filter
6.2.1 CQRS Write and Read Pipelines
Why would you build two completely separate pipelines for one system? In the RAJ architecture, documents go in (ingestion) and answers come out (query). These are fundamentally different operations — one writes to a database, the other reads from it. Treating them as one pipeline forces unnecessary coupling. What if we could design them as independent, specialized workflows?
The professor modeled the RAJ architecture using two established software design patterns simultaneously: the pipe-and-filter pattern and CQRS (Command Query Responsibility Segregation). Understanding why both apply — and how they complement each other — is the key architectural lesson of this section.
CQRS (Command Query Responsibility Segregation) is a design pattern that separates the write side (commands that change state) from the read side (queries that retrieve state) into distinct pipelines, models, and often separate code files. The word "segregate" means to set apart — CQRS literally means "separate the responsibility of writing from the responsibility of reading."
Analogy — a library's intake desk vs. reading room. When a library receives new books, the intake desk processes them: cataloguing, labeling, shelving (the write side). When a patron wants a book, they visit the reading room and ask a librarian to find it (the read side). The intake desk and the reading room serve completely different purposes, use different tools, and are staffed by different people — but they share the same collection. CQRS applies the same principle to software: separate the ingestion workflow from the query workflow.
The textbook (T1 Ch08) identifies this as a core architectural challenge: "Decomposing the system and deciding how to divide the work is a key step." CQRS provides a principled way to decompose RAJ along the command/query boundary.
CQRS in RAJ — write pipeline vs query pipeline
On the write side (the ingestion pipeline), data flows through a sequence of filters connected by pipes — this is the pipe-and-filter pattern:
- Documents are loaded from source files.
- Documents are split into chunks.
- Chunks are embedded into vectors.
- Vectors are persisted into a vector store.
On the read side (the query pipeline), a different sequence operates:
- The user enters a prompt.
- The prompt is embedded using the same embedding model.
- ChromaDB retrieves the most similar chunks.
- The retrieved chunks and the user's query are sent to an LLM which generates a grounded response.
These two pipelines are implemented as separate files: ingest.py for writes and
app.py (with a read module) for queries. They share only the vector store — the read side never
modifies it, and the write side never queries it.
Why CQRS fits RAJ perfectly. The write pipeline is batch-oriented (process many documents at once, run once, then stop). The read pipeline is interactive (process one user query at a time, run continuously). These are different workloads with different scaling characteristics. CQRS lets you optimize each independently.
The write model (ingest.py) performs three sequential activities:
Each step is a filter in the pipe-and-filter pattern — it takes input, transforms it, and passes the result to the next step.
Step 1 — Document loading. Load PDFs (or PPTs, Word files) using appropriate document loaders. LangChain provides loaders for each format. The loader reads the file and produces a list of document objects — each object contains the text content plus metadata (source file name, page number).
Step 2 — Document splitting. Use a text splitter — the recursive character text splitter is the standard choice — to break documents into chunks. The demo uses a chunk size of 1,000 characters with a chunk overlap of 200 characters. Both parameters are configurable and represent a trade-off:
- Larger chunks (e.g., 2,000 characters) capture more context per chunk but reduce retrieval precision — the system might retrieve a chunk where only part is relevant.
- Smaller chunks (e.g., 500 characters) improve precision but may lose context that spans chunk boundaries.
- Overlap mitigates the boundary problem: the 200-character overlap ensures that information near a chunk boundary appears in both adjacent chunks, so it is not lost.
Step 3 — Building the vector store. Each chunk is embedded using OpenAI's
text-embedding-3-small model and persisted in ChromaDB. The chunk becomes a document object, an
embedding vector is computed (a list of numbers representing the semantic meaning of the text), and both are
stored together. ChromaDB indexes these vectors for fast similarity search.
Worked example — the full write pipeline on a single PDF:
Suppose you have a 10-page PDF lecture slide deck.
- Load: The PDF loader reads all 10 pages and produces 10 document objects, one per page,
each containing the page's text and metadata
{"source": "lecture06.pdf", "page": 3}. - Split: The text splitter breaks each page's text into chunks of 1,000 characters with 200-character overlap. A page with 2,500 characters produces 3 chunks (characters 0–1000, 800–1800, 1600–2500). Across all 10 pages, suppose this yields 25 chunks total.
- Embed and store: Each of the 25 chunks is sent to the embedding model, which returns a
vector of 1,536 dimensions (the output size of
text-embedding-3-small). Each vector is stored in ChromaDB along with its source chunk text and metadata. After this step, the vector store contains 25 searchable entries.
The PDF is now "ingested." The write pipeline is complete and can be shut down — it does not need to run during queries.
The read model (query pipeline)
The application (app.py) provides a user interface — Streamlit in the demo —
where the user enters a prompt. The query pipeline then executes:
- The user's prompt is embedded using the same
text-embedding-3-smallmodel (critical: the embedding model must be identical on both sides, or the vectors will live in different semantic spaces and similarity search will fail). - ChromaDB retrieves the most similar chunks using cosine similarity (or another distance metric).
- Those chunks, along with the user's query, are sent to an LLM — GPT-4o mini via the OpenAI API — which produces a grounded, well-formatted answer.
The LLM's role is specifically to handle the language aspect: taking the raw retrieved data and reframing it in a natural, coherent response that matches the phrasing of the user's prompt. The LLM handles vocabulary, grammar, coherence, and summarization — everything regarding how the answer is expressed. The factual substance comes from the retrieved documents, not from the LLM's parametric knowledge.
No generation during ingestion. The first half of RAJ — the ingestion pipeline — does not use an LLM at all. The LLM appears only in the final retrieval-and-display stage. Until that point, RAJ is purely a retrieval system: it finds the most relevant chunks from the stored documents. The generation happens only when the LLM receives those chunks and the user query together. This separation is architecturally important: ingestion is deterministic and repeatable; generation is probabilistic and creative.
Scope — when CQRS is overkill for RAJ. CQRS adds complexity: two codebases to maintain, two deployment pipelines, shared state (the vector store) to keep consistent. For a small prototype or a single-user tool, a single script that handles both ingestion and querying may be simpler. CQRS becomes valuable when:
- The ingestion and query workloads scale differently (batch vs interactive).
- Different teams own the write and read sides.
- You want to update the model or embedding strategy without touching the query code.
Common pitfall — using different embedding models on write and read sides. If
ingest.py uses text-embedding-3-small but app.py uses a different model
(say, a local Sentence Transformer), the query vector and the stored vectors will live in completely different
geometric spaces. Cosine similarity between them is meaningless. Always ensure the same embedding model is
used on both sides.
Real-world connection. The CQRS pattern is used extensively in production ML systems. Netflix's recommendation pipeline, for example, separates the batch job that computes and stores user embeddings (write side) from the real-time service that retrieves recommendations (read side). The same pattern applies to any system where data ingestion and data querying have different performance, scaling, or reliability requirements.
6.3 RAJ Demo: Document Ingestion and Query Pipeline
6.3.1 Live Demo Walkthrough
Can the RAJ pipeline actually work on real course materials? The professor demonstrated the full end-to-end pipeline — ingestion and querying — using the course's own lecture slides. This section walks through that demo with concrete numbers and shows what happens when the system encounters a question it cannot answer.
The live demonstration processes five session PPTs (converted to PDF) with the following statistics:
| Document | Pages |
|---|---|
| Session 1 | 55 |
| Session 2 | 49 |
| Session 3–5 | 83 |
| Total pages loaded | 187 |
| Total chunks after splitting | 183 (chunk size 1,000, overlap 200) |
The fact that 187 pages produce 183 chunks — roughly one chunk per page — tells us something about the average content density of the slides. Lecture slides tend to have short bullet points, so many pages produce fewer than 1,000 characters and are not split at all. Dense text pages (like reference material) would produce multiple chunks per page.
Ingestion phase (the write pipeline)
The ingest.py script executes the three-step CQRS write pipeline described in the previous
section:
- Load: Each PDF is read page by page using LangChain's PDF loader. The loader produces one document object per page with metadata including the source file name and page number.
- Split: The recursive character text splitter breaks each page's content into chunks of 1,000 characters with 200-character overlap.
- Embed and store: Each chunk is embedded using
text-embedding-3-small(1,536-dimensional vectors) and persisted in ChromaDB.
Once ingestion is complete, the vector store exists on disk (or in memory). The ingestion script can be shut down — it has done its job. The Streamlit app loads the existing vector store without re-ingesting, which is the CQRS principle in action: the write side and read side are separate processes.
Worked example — querying "What is RAJ model?":
- The user types "What is RAJ model?" into the Streamlit interface.
- The query is embedded using
text-embedding-3-small, producing a 1,536-dimensional vector. - ChromaDB performs a similarity search across all 183 stored chunk vectors and returns the top- most similar chunks. In this demo, the relevant chunks come from sessions 3 and 5 — the sessions where RAJ was actually taught.
- Each retrieved chunk includes metadata: the source PDF file name and the page number.
- The retrieved chunks and the user's query are sent to GPT-4o mini with a system prompt instructing it to answer only from the provided context.
- The LLM produces a coherent answer about the RAJ model, grounded in the retrieved content, and the Streamlit app displays the answer along with source citations (PDF name + page numbers).
The student can verify the grounding by checking the cited pages in the original slides.
Boundary behavior — out-of-context queries
The professor tested a deliberately out-of-scope question to demonstrate how RAJ handles queries that fall outside the ingested documents.
Worked example — querying "What is the weather today?":
- The user asks "What is the weather today?"
- The query is embedded and ChromaDB retrieves chunks — but none of the 183 chunks from lecture slides contain weather-related content. The retrieved chunks have low similarity scores.
- The LLM receives these low-relevance chunks along with the weather question.
- Because the system prompt instructs the LLM to answer only from the retrieved context, the LLM responds: "The information provided does not include any details about the weather. I cannot answer your question."
This is the correct behavior: the system refuses to hallucinate. It does not reach into its parametric knowledge to answer a question that the documents do not cover. This is a fundamental property of well-designed RAJ systems — they are grounded in the source documents.
Why source citation matters. Every answer includes the source PDFs and page numbers from which the information was retrieved. This allows the user to verify the grounding of each response — to check whether the LLM's answer actually reflects what the documents say. Source citation is not just a nice-to-have; it is a trust mechanism. Without it, the user has no way to distinguish a grounded answer from a hallucinated one.
Configuration and flexibility
The demo uses:
- GPT-4o mini for generation (the LLM that formulates answers).
text-embedding-3-smallfor embeddings (the model that converts text to vectors).- Both accessed via OpenAI API key — no models are downloaded locally.
Students are free to experiment with alternatives:
- Local models: Mistral 7B, Gemma 2B (which works well for small document sets when run locally).
- API-based models: Gemini, Claude, or any other LLM via API.
- Local embedding models: Sentence Transformers, BGE embeddings, or other open-source options.
The architectural pattern remains the same regardless of which specific models you use — only the API endpoints and model names change.
Common pitfall — mixing API and local models without adjusting expectations. GPT-4o mini is a capable model that handles retrieved context well. Smaller local models (like Gemma 2B) may struggle with complex queries or produce less coherent answers, especially when the retrieved context is long. If you switch to a smaller model, consider reducing the number of retrieved chunks (lower ) to keep the context window manageable.
Real-world connection. This demo is a miniature version of what companies like Notion, Slack, and Google build for their internal knowledge-base search features. The pattern — ingest documents, embed, retrieve, generate with citations — is the standard architecture for enterprise search and question-answering systems. The key difference in production is scale (millions of documents instead of 187 pages) and operational concerns (monitoring, access control, incremental updates).
6.4 RAJ Student Q&A
6.4.1 Question and Answer Exchanges
The professor's demo triggered several student questions that surfaced important architectural concepts. These Q&A exchanges are grouped by confusion point — each addresses a distinct concern about how RAJ works in practice.
Q: Does the system handle follow-up questions using conversation history?
A: The conversational history is stored in ChromaDB. The system answers based not only on the current prompt but also on prior exchanges. However, this demonstration focuses on the architectural pattern rather than on guardrails or full conversational depth. Extensions like guardrails are left for student exploration in assignments.
This question reveals a common assumption: that RAJ is stateless. In the basic demo, each query is treated independently. But production conversational RAJ systems do maintain history — typically by storing previous exchanges in the same vector store (ChromaDB) and retrieving them alongside document chunks. The system can then answer "What did we just discuss?" by retrieving the conversation history chunks.
Q: Can SQL databases, CSVs, or Word files serve as input sources?
A: Absolutely. LangChain is highly modular. The input can be a PDF, a SQL database, a CSV file, a Word document, or any combination of these. The only requirement is that you use the appropriate document loader and ultimately produce chunks for embedding. The architecture is completely decoupled from the input format.
This is the adapter pattern in action. LangChain provides a library of document loaders — one for each format. The loader's job is to convert any input format into a uniform list of document objects. Once that conversion happens, the rest of the pipeline (split, embed, store) does not know or care what the original format was.
Q: What is the role of embeddings? Does RAJ work without embeddings?
A: Without embeddings, retrieval is keyword-based only. Embeddings introduce semantic search — the ability to find chunks that are meaning-related to the query, even if they share no exact words.
Consider the professor's example: a keyword search for "Salman Khan" returns only pages containing that exact string. A semantic search understands that "Salman Khan" is an actor and can also surface "Aamir Khan" or "Akshay Kumar" as related results — because the embedding vectors for these names cluster together in the vector space. Similarly, the word "king" carries the semantic attribute "male," while "queen" carries "female" — this relational meaning is what semantic embeddings capture and what keyword search misses entirely.
For very small document sets (a few pages), a powerful LLM given the full context might answer correctly without embeddings. But as the document corpus grows, semantic search becomes essential for retrieving the right chunks rather than just keyword-matched chunks.
Semantic vs keyword search — the core trade-off. Keyword search is fast, deterministic, and works well when users know the exact terms in the documents. Semantic search is slower (requires embedding computation) but handles paraphrasing, synonyms, and conceptual relationships. In practice, production systems often use a hybrid approach: keyword search for exact matches (product codes, error messages) combined with semantic search for conceptual queries.
Q: If a document never abbreviates "SE for ML" but the user writes that in the query, will the system map it to the relevant chunks?
A: In the standard RAJ architecture demonstrated here, the system will not automatically infer abbreviations or synonyms that are absent from the ingested documents. The retrieved chunks must contain the terms present in the query (or their semantic neighbors captured by embeddings).
This is precisely why agentic RAJ has emerged: when the retrieved context is insufficient, an intermediate agent layer can dynamically fetch missing information through external tools before formulating the response. The agent itself is an LLM that receives input, decides whether to invoke tools, and augments the context accordingly. Agentic layers can be inserted anywhere in the pipeline:
- Before chunking: to clean or augment input documents (expand abbreviations, resolve references).
- After retrieval: to enrich the context with external knowledge when the retrieved chunks are insufficient.
This question and answer introduce a critical architectural evolution. Standard RAJ is a fixed pipeline — data flows in one direction. Agentic RAJ adds a decision-making layer that can dynamically adjust the pipeline's behavior based on the query. This is a step toward more autonomous AI systems.
Q: Is the LLM generating new information in RAJ, or only retrieving?
A: The LLM is not generating new factual content beyond what is in the documents. Its role is to take the raw retrieved data and present it coherently — framing the answer, summarizing if asked, removing redundancy, and matching the response style to the prompt. If the document contains 150 words and the user asks for a 50-word summary, the LLM compresses it. If the raw answer is a jumble of sentence fragments, the LLM organizes it into fluent prose. But the factual substance comes from the retrieved documents, not from the LLM's parametric knowledge.
Exam note: The LLM in RAJ is a language processor, not a knowledge source. It reformulates retrieved data — handling grammar, coherence, summarization, and style — but the facts come from the documents. If the documents do not contain the answer, the system should refuse to answer rather than hallucinate.
Common pitfall — confusing the LLM's role in RAJ with its role in standalone chat. In a standalone chat (no retrieval), the LLM draws entirely on its parametric knowledge — everything it learned during training. In RAJ, the LLM is constrained to the retrieved context. These are fundamentally different operating modes, and conflating them leads to incorrect assumptions about what the system can and cannot answer.
Real-world connection. The distinction between retrieval-grounded generation and free-form generation is central to enterprise AI adoption. Companies using RAJ for internal knowledge bases need assurance that the system will not fabricate answers about company policies, financial data, or legal requirements. The "retrieve first, generate second" architecture provides this assurance by design.
6.5 Monolithic Architecture — Software Engineering Foundations
6.5.1 Definition and Food-to-Go Example
Why do most startups begin with a monolith — and why do they eventually outgrow it? The answer reveals a fundamental tension in software architecture: simplicity now versus scalability later. Every architectural decision is a trade-off, and the monolith is the oldest and most common starting point.
Monolithic architecture means the application is composed as one single piece. The user interface, business logic, data access layer, and any service layer are tightly wrapped into one codebase, deployed as one unit, and connected to a single shared database. The word "monolith" comes from Greek: mono (one) + lithos (stone) — a single, uncarved block.
The textbook (T1 Ch08) defines this precisely: "The system is composed of a single unit where internals are interwoven rather than separated. Internally there might be modules and libraries, but they are usually not intentionally arranged as services or in layers." The key word is interwoven — the modules exist, but they are not isolated from each other.
Analogy — a restaurant kitchen where one chef does everything. Imagine a kitchen where a single chef handles taking orders, cooking, plating, billing, and serving. It works fine when there are 5 customers. But when 50 arrive simultaneously, the chef cannot scale — you cannot hire a second "order-taking chef" without also duplicating cooking, plating, and billing. Everything is coupled in one person. A monolithic application has the same constraint: you can only scale the whole thing, not individual parts.
The Food-to-Go (Swiggy/Zomato) example. The professor used a food delivery application to make monolithic architecture concrete. The application involves three actors: the customer (consumer), the delivery agent (courier), and the restaurant. The system is organized into modules:
- Restaurant management
- Order management
- Delivery management
- Payment
- Notification
- Billing
All modules reside in the same codebase, written in the same language (Java in the example), and share one MySQL database. External cloud services (Twilio for messaging/SMS, AWS SES for email, Stripe or Razorpay for payment) are integrated through adapters, but the core application remains a single block.
Worked example — a complete order flow in the monolith:
- Customer opens the app and browses restaurants (restaurant management module).
- Customer places an order (order management module writes to MySQL).
- Restaurant accepts the order (order management module updates status in MySQL).
- Payment is processed (payment module calls Stripe API, records transaction in MySQL).
- Delivery agent is assigned (delivery management module queries MySQL for available agents).
- Notifications are sent (notification module calls Twilio/SMS and SES/email).
- Delivery is marked complete (delivery management module updates MySQL).
Every step happens within a single Java process. Module-to-module calls are in-process function calls — no network latency, no serialization overhead. The entire flow can be tested end-to-end by deploying one artifact and running one test suite.
Advantages of monolithic architecture:
- Shared memory and speed. The entire application runs as one process (e.g., on a single EC2 instance), so components call each other in-process, making inter-module communication fast. There is no network hop between the order module and the payment module — they share the same memory space.
- Straightforward end-to-end testing. A tester can trace one complete flow — login, create order, restaurant accepts, payment processed, delivery completed, notifications received — all within one deployable unit. Testing is easier because everything is co-located and there are no distributed system failure modes to worry about.
- Simple deployment. One artifact to build, one artifact to deploy. No orchestration, no service discovery, no load balancing between services.
Disadvantages:
- Technology lock-in. The entire application is written in one language stack (e.g., Java). Every developer must work in that stack. You cannot use Python for the ML recommendation engine and Java for the payment service — it is all one codebase.
- Scalability is the biggest problem. When load increases, the entire application must be scaled as one unit — you cannot scale only the payment module or only the order module independently. If Black Friday drives 100× more payment traffic but only 2× more restaurant browsing traffic, you still have to scale the entire application 100×. This wastes resources on the modules that did not need scaling.
The Flipkart Big Billion Day failure (2014). The professor cited this as the canonical example of monolithic scalability limits. During Flipkart's annual sale event, traffic surged far beyond what the monolithic architecture could handle. The entire system went down — not just the checkout or payment modules, but everything, because it was all one unit. The lesson: monolithic architectures have a ceiling, and that ceiling applies to the weakest module, not the strongest.
Contrast: Hotstar (2019). Hotstar scaled to 25 million concurrent users during an India vs New Zealand cricket match. This was possible because Hotstar had migrated to a microservices architecture, allowing independent scaling of the video streaming, chat, and score-update services. The same scale would have been impossible with a monolith.
- Difficult to understand and maintain. A large codebase accumulated over years becomes impenetrable for newcomers. The professor cited a real-world example: a legacy VB (Visual Basic) application with 660 database tables, where even a small change risked breaking unrelated functionality. When modules are interwoven, changing one part can have unpredictable ripple effects across the entire system.
The textbook (T1 Ch08) reinforces this with the Twitter case study: "Twitter was originally designed as a monolithic database-backed web application, written in Ruby on Rails by three friends. Once Twitter became popular, it became slow and hard to scale. Developers introduced caches throughout the application and bought many machines to keep up with the load, but they could not handle spikes in traffic." Twitter eventually had to completely redesign the system — a costly and rare undertaking.
Common pitfall — assuming monolithic means bad. Monolithic architecture is not inherently flawed. For a startup with 3 developers, a prototype, or an application that will never need to scale independently, a monolith is the right choice. The problems emerge at scale — when the codebase grows large, when traffic demands independent scaling, when teams need to deploy independently. Choosing a monolith early and migrating later is a valid strategy; over-engineering with microservices from day one is often worse.
The fundamental tension. Monoliths are simple to start but painful to scale, both technically and organizationally. The textbook frames this as an architectural trade-off: "When Twitter was first started, scalability was likely less important than releasing a prototype quickly to gain venture funding and users, so the monolithic Ruby application may have been appropriate at a time, just not future proof given how difficult to change architectural decisions are later."
Real-world connection. Most successful tech companies — Amazon, Netflix, Twitter, Uber — started as monoliths and migrated to microservices as they grew. The monolith-to-microservices migration is one of the most common architectural transitions in the industry. The key lesson is not "avoid monoliths" but "know when you have outgrown one."
6.6 Monolithic ML Pattern and Iris Demo
6.6.1 Monolithic ML Pattern
How does the monolithic concept translate to machine learning systems? The same architectural logic applies, but with a twist: ML systems have additional components — preprocessing, model training, model serving — that create new coupling points and new scaling pressures.
In the monolithic ML pattern, all ML components — preprocessing, model loading, prediction logic, and serving (UI or API) — are tightly coupled and deployed as a single unit. The model is a library embedded in the application, not an independent service.
The textbook (T1 Ch08) identifies this as one of the common system structures: "Machine-learning components may be interwoven in such systems, often using libraries. System development is initially simple and local without the need for networked communication and the complexities of distributed systems."
Advantages:
- Simple to develop and deploy. Everything is in one place. A data scientist can train a model, serialize it (save it to a file), load it in a FastAPI app, and ship it — all in a single codebase.
- Low operational overhead. One process to monitor, one container to deploy, one log stream to watch. No service discovery, no load balancing, no distributed tracing.
- Complete control over the project. Every component is accessible and modifiable in the same repository.
- Easier end-to-end testing. You can test the full flow — send input, get prediction — with a single test script against a single endpoint.
- Appropriate for: prototypes, small projects, hackathons, internal tools, or initial exploration where the application will not grow significantly.
Disadvantages:
- Cannot scale independently. If prediction load surges (100× more requests at peak hours), the entire application must scale as a monolith — you cannot scale only the inference service while keeping the preprocessing or UI unchanged. This is the same scalability problem as the general monolith, but applied to ML workloads.
- Model updates require full redeployment. Even a minor model version change (e.g., version 5.1 to 5.2 with slightly improved accuracy) forces redeployment of the complete application — the API, the UI, the preprocessing logic, everything. In a microservices architecture, you would redeploy only the model service.
- Technology lock-in. In practice, most ML applications use Python, so language diversity is less of an issue than in general software. But flexibility to adopt new technologies within components is still restricted — you cannot easily swap the serving framework from FastAPI to something else without touching the entire codebase.
- Single point of failure. Any error anywhere in the application — a bug in preprocessing, a null pointer in the API handler, a model loading failure — can bring the entire system down. There is no isolation between components.
Scope — when the monolithic ML pattern breaks down. The pattern is viable when:
- You have one model serving one endpoint.
- Traffic is predictable and does not require independent scaling.
- Model updates are infrequent.
- The team is small (1–3 developers).
It breaks down when any of these conditions change: multiple models, bursty traffic, frequent retraining, or larger teams that need to work independently.
Iris classification demo — the monolithic ML pattern in code
The professor demonstrated the monolithic ML pattern using the classic Iris dataset — four input features (sepal length, sepal width, petal length, petal width) predicting three flower classes: setosa (0), versicolor (1), and virginica (2).
A pre-trained logistic regression model is serialized as a pickle file
(logistic_regression_model.pkl). Pickle is Python's built-in serialization format — it
converts a Python object (like a trained model) into a byte stream that can be saved to disk and loaded later.
The application, built with FastAPI and served via Uvicorn, does the
following:
- Loads the pickle model at startup.
- Exposes a single
POST /predictendpoint. - Accepts a JSON list of four feature values.
- Calls
model.predict(features)and returns the predicted class (0, 1, or 2).
Worked example — a complete prediction request:
The user sends a POST request to localhost:5000/predict with the following JSON body:
{"features": [5.1, 3.5, 1.4, 0.2]}
This represents an Iris flower with:
- Sepal length: 5.1 cm
- Sepal width: 3.5 cm
- Petal length: 1.4 cm
- Petal width: 0.2 cm
The application:
- Parses the JSON to extract the four feature values.
- Reshapes them into a 2D array (required by scikit-learn: shape
[1, 4]). - Calls
model.predict([[5.1, 3.5, 1.4, 0.2]]). - The logistic regression model returns class
0(setosa). - Returns
{"prediction": 0}as JSON.
The Swagger UI at /docs provides an interactive form where users can input the four numerical
parameters and receive the classification without writing any code.
The textbook (T2 Ch11) explains this pattern: "If this becomes a feature in your company's product, your model may need to receive input data and return predictions many times a second. You can build an API that waits for that data and returns the prediction." FastAPI with Uvicorn is the de facto standard for this in Python — it handles JSON serialization, input validation (via Pydantic), and auto-generates interactive documentation.
Everything — model loading, prediction logic, input handling, output formatting — lives in a single code file and a single process. This is monolithic in the purest sense: one unit for model serving, with no separation between model management and inference.
The monolithic ML pattern is the starting point for most ML deployments. Every ML application starts here: train a model, serialize it, wrap it in an API. The question is not whether to start with a monolith, but when to migrate away from it. The next section (6.7) shows what the alternative looks like.
Real-world connection. FastAPI + Uvicorn is used throughout the industry for rapid ML model prototyping. Companies like Netflix, Uber, and Microsoft use this pattern for internal tools and early-stage products before migrating to more distributed architectures as the product matures.
6.7 Microservices Architecture — Foundations and Industry Impact
6.7.1 Principles and Industry Adoption
If monoliths are simple, why did every major tech company abandon them? The answer is scale — not just technical scale (handling millions of requests), but organizational scale (hundreds of developers deploying independently). Microservices architecture emerged as the solution to both problems.
Microservices architecture decomposes an application into a collection of independently deployable services, each following the Single Responsibility Principle (SRP). The principle, from Robert C. Martin, states: gather together things that change for the same reason; separate things that change for different reasons. In microservices terms: if two components scale differently, deploy differently, or are maintained by different teams, they should be separate services.
The textbook (T1 Ch08) defines this precisely: "A system is organized into multiple self-contained services (processes) that call other services through remote procedure calls. The services are not necessarily organized into layers, and typically each service represents a cohesive piece of functionality and is responsible for its own data storage. This design allows independent deployment, versioning, and scaling of services and flexible routing of requests at the network level."
Analogy — a food court vs a single restaurant. In a food court, each stall (pizza, sushi, burgers) operates independently — it has its own kitchen, its own staff, its own inventory. If the sushi stall runs out of fish, the pizza stall keeps serving. Each stall can hire more staff during lunch rush without affecting others. A monolithic restaurant, by contrast, has one kitchen — if the oven breaks, no one can cook anything. Microservices are the food court model: independent units that share a building (the API gateway) but not a kitchen.
Revisiting the Food-to-Go example under microservices. The same three actors — customer, courier, restaurant — interact not with a single monolithic block but through an API gateway. The gateway is a single entry point that routes each request to the appropriate microservice:
| Service | Responsibility | Database |
|---|---|---|
| Order service | Create, update, track orders | MySQL (relational — structured order data) |
| Restaurant service | Manage menus, availability, accept/reject orders | MongoDB (NoSQL — flexible menu documents) |
| Accounting service | Process payments, generate invoices | MySQL |
| Notification service | Send SMS, email, push notifications | Redis (in-memory — fast, ephemeral data) |
| Delivery service | Assign agents, track delivery status | MySQL |
Each service owns its own database — this is a critical design rule. Services do not share databases, because shared databases create hidden coupling (changing the schema in one service breaks queries in another). Each service communicates with external providers through its own adapters.
One service can invoke another service:
- Synchronously via REST or gRPC (the caller waits for the response).
- Asynchronously through message queues like Kafka (the caller sends a message and continues without waiting).
The API gateway handles routing and aggregation, but each service is developed, tested, deployed, and scaled independently.
Worked example — the same order flow, now as microservices:
- Customer sends a POST request to the API gateway at
gateway.foodtogo.com/orders. - The gateway routes the request to the order service.
- The order service creates the order in its MySQL database and sends a message to the restaurant service.
- The restaurant service accepts the order and publishes an "order-accepted" event.
- The accounting service subscribes to "order-accepted" events and processes payment via Stripe.
- The delivery service subscribes to "payment-completed" events and assigns a courier.
- The notification service subscribes to all events and sends SMS/email updates at each step.
Each step happens in a different service, potentially on different machines, potentially written in different languages. The order service does not know or care how payment is processed — it just publishes an event and moves on.
Industry-scale evidence of microservices benefits:
Amazon and Google deploy thousands of times per day. Amazon performs approximately 23,000 deployments per day across all services. Google performs approximately 5,500 deployments per day. This is only possible because each service is deployed independently — a developer who fixes a bug in the recommendation service deploys only that service, not the entire Amazon platform. Deployment lead time is measured in minutes rather than hours.
The textbook (T1 Ch08) uses Twitter's migration as a case study: after redesigning from a monolith to microservices, "the new system was more complex (inherent in distributed systems) and more costly to develop, but this was deemed a necessary trade-off for achieving the primary four quality goals" — latency, reliability, maintainability, and modifiability.
Key advantages:
- Polyglot development. Each microservice can use the most appropriate language and framework. Instagram, for example, is built with Python, React Native, and JavaScript. One service might use Java with Spring Boot (robust, enterprise-grade), another Python (rapid prototyping, ML integration), and another C (performance-critical path).
- Polyglot persistence. Each microservice can choose its own database type — relational (MySQL, PostgreSQL), NoSQL (MongoDB, Cassandra), or in-memory (Redis) — based on its specific data access patterns. The order service needs ACID transactions (relational); the notification service needs fast key-value lookups (Redis).
- Independent scalability. This is the most important advantage. The professor's example: if the recommendation service on YouTube goes down, users can still play videos, write comments, and download content because those services are independent. Similarly, if video streaming traffic spikes 100× during a live event, only the streaming service scales — the comment and recommendation services remain at their current capacity.
- Faster development velocity. Teams can work on different services in parallel without stepping on each other's code. No merge conflicts, no "who broke the build" blame games, no waiting for the other team to finish their feature before you can deploy yours.
- High reliability. The failure of one service does not cascade into a total system outage (provided dependencies are well-managed). This is called fault isolation — a bug in the notification service does not prevent users from placing orders.
Common pitfall — the distributed monolith. If microservices are tightly coupled — service A cannot function without service B being available, they share a database, or they must be deployed together — you have not gained anything over a monolith. You have the complexity of distributed systems (network failures, latency, debugging difficulty) without the benefits (independent scaling, deployment, development). This anti-pattern is called a distributed monolith, and it is the worst of both worlds.
Synchronous communication in microservices. Services typically communicate synchronously through:
- REST APIs — the most common, using HTTP with JSON. Simple, well-understood, universally supported.
- gRPC — using Protobuf as the binary data-exchange format over HTTP/2. Faster and more resource-efficient than REST, but requires both sides to implement the same Protobuf contract.
- GraphQL — enables a single request returning data from multiple backend services. Popular in frontend-heavy applications.
Asynchronous communication uses message brokers like Kafka, RabbitMQ, or cloud-managed services (Amazon SQS, Azure Service Bus, Google Pub/Sub). Section 6.10 covers event-driven architecture in detail.
Real-world connection. The microservices pattern is the dominant architecture for large-scale web applications. Netflix (hundreds of microservices), Uber (thousands of microservices), and Amazon (tens of thousands of microservices) all use this pattern. The trade-off is operational complexity: microservices require sophisticated tooling for service discovery, load balancing, distributed tracing, and failure handling — tooling that monoliths do not need.
6.8 Synchronous Communication: REST, GraphQL, and gRPC
6.8.1 Comparing API Protocols
When microservices need to talk to each other, what language do they speak? The choice of communication protocol — REST, GraphQL, or gRPC — affects performance, developer experience, and system complexity. A student question about gRPC versus GraphQL prompted an extended comparison of the three primary synchronous API styles relevant to ML systems.
In a microservices architecture, services communicate over a network. The protocol they use determines how requests are formatted, how responses are structured, and how fast the communication is. The professor compared three options: REST, GraphQL, and gRPC.
REST (Representational State Transfer)
REST is the most widely used approach: approximately 90% of ML applications use REST. It was defined by Roy Fielding in 2000 as a software architectural style for distributed systems.
REST operates over HTTP with JSON as the data-exchange format. The textbook (T2 Ch11) explains: "RESTful APIs are the most common... A RESTful API uses HTTP methods to make a request, then returns a response as a JSON file." The four standard HTTP verbs cover most API needs:
| HTTP Verb | Purpose | Example |
|---|---|---|
| GET | Retrieve data | GET /models/iris/predictions |
| POST | Create or submit data | POST /predict with feature values |
| PUT | Update existing data | PUT /models/iris with new config |
| DELETE | Remove data | DELETE /models/iris/versions/1 |
REST calls are individual: each call retrieves or submits a specific resource. AWS Bedrock, OpenAI's API, and most cloud ML services expose REST APIs.
Why REST dominates ML serving. REST is universally understood, well-supported by frameworks like FastAPI and Flask, and sufficient for most inference workloads. The textbook (T2 Ch11) demonstrates building REST APIs with FastAPI: "FastAPI is a great choice for building your own APIs... it conforms with the OpenAPI specifications, a widely used set of standards for APIs." FastAPI auto-generates Swagger UI documentation, handles JSON serialization, and validates input via Pydantic — making it the de facto standard for ML model serving in Python.
GraphQL
Developed by Facebook (now Meta), GraphQL addresses REST's limitation when a client needs data from multiple resources.
The problem GraphQL solves. In REST, fetching a user's posts and all comments on those posts
requires two separate API calls — one to /users/123/posts and another to
/posts/456/comments. If the client also needs the user's profile and the likes on each post, that
is two more calls. Each call has network latency, and the client must assemble the results.
In GraphQL, a single request to the GraphQL endpoint specifies exactly what data is needed:
query {
user(id: 123) {
name
posts {
title
comments { text author { name } }
likes { count }
}
}
}
The server returns a unified response containing all requested fields — posts with nested comments, likes, and author names — in one round trip.
The request hits any number of backend data sources (DynamoDB table, S3 bucket, relational database, another REST API) and collates everything into one response. This "single request, all-inclusive response" pattern makes GraphQL especially popular in social media applications where data is deeply nested and clients need flexible queries.
There is no inherent limit on response size; pagination and lazy loading can be implemented at the presentation layer — GraphQL supports both.
gRPC (Google Remote Procedure Call)
gRPC extends the traditional RPC (Remote Procedure Call) concept: the caller treats a remote service as if it
were a local function call. Instead of thinking in terms of HTTP resources (GET /users, POST /orders), the
developer thinks in terms of function calls: getUser(123), createOrder(orderData).
The client defines a contract (a .proto file) specifying the service methods and
their input/output message formats. Both client and server must adhere to this contract. The contract looks
like:
service PredictionService {
rpc Predict(PredictionRequest) returns (PredictionResponse);
}
message PredictionRequest {
repeated float features = 1;
}
message PredictionResponse {
int32 prediction = 1;
float confidence = 2;
}
gRPC uses Protobuf (Protocol Buffers) — a binary serialization format — instead of JSON, and operates over HTTP/2, which supports bidirectional streaming.
| Feature | REST | GraphQL | gRPC |
|---|---|---|---|
| Data format | JSON (text) | JSON (text) | Protobuf (binary) |
| Protocol | HTTP/1.1 | HTTP/1.1 | HTTP/2 |
| Contract | OpenAPI/Swagger (optional) | Schema (required) | .proto file (required) |
| Speed | Moderate | Moderate | Fast (binary + HTTP/2) |
| Ease of use | Easy | Moderate | Moderate (requires .proto setup) |
| Best for | General ML serving | Multi-resource frontend queries | High-performance internal services |
Scope — course focus. In this course, the focus is on REST. gRPC is gaining adoption in ML systems (Google Cloud ML, TensorFlow Serving use gRPC), and GraphQL has niche applications (social media frontends), but they are outside the course scope. The comparison here is for architectural awareness, not implementation.
Q: How do REST, GraphQL, and gRPC compare, and when do you choose each?
A: REST is used by approximately 90% of ML applications over HTTP/JSON — it is the pragmatic default. GraphQL enables a single request returning an all-inclusive response from multiple backend data sources — popular in social media applications with nested data. gRPC uses binary Protobuf over HTTP/2, making it the fastest and most resource-efficient option, but both sides must implement the same Protobuf contract.
Q: Are there restrictions on GraphQL response size or pagination?
A: No inherent limit on response size. Pagination and lazy loading can be implemented at the presentation layer — GraphQL supports both. The server can implement cursor-based or offset-based pagination to manage large result sets.
Q: Which protocol is best for server resource consumption?
A: gRPC is best because it uses binary format and HTTP/2 protocol, making it significantly faster and more resource-efficient than REST or GraphQL. Binary serialization is more compact than JSON (smaller payloads), and HTTP/2 multiplexing reduces connection overhead.
Exam note: REST is the default for ML serving (~90% adoption). GraphQL excels at multi-resource frontend queries. gRPC is fastest (binary + HTTP/2) but requires Protobuf contracts on both sides. Know the trade-offs, not the implementation details.
Real-world connection. Google uses gRPC extensively for internal service communication (it was invented there). Netflix uses a mix of REST (for external APIs) and gRPC (for internal service-to-service calls). Facebook/Meta uses GraphQL for its mobile apps (which need to fetch deeply nested data in one request). For ML model serving, REST via FastAPI remains the standard choice for most teams.
6.9 Microservices ML Pattern and Demo
6.9.1 ML-Specific Microservices and Implementation
How do you break a monolithic ML application into microservices? The Iris monolith from section 6.6 — one file, one process, one endpoint — becomes three independent services. The professor demonstrated this refactoring live, and the architectural lessons go far beyond the demo code.
Microservices ML Pattern. The core principle of microservices extends cleanly to ML systems: each microservice focuses on one specific ML functionality. The textbook (T1 Ch08) identifies this as a core architectural challenge: "Decomposing the system and deciding how to divide the work is a key step." In ML systems, the natural decomposition follows the ML lifecycle:
| Service | Responsibility | Scaling trigger |
|---|---|---|
| Data ingestion service | Read raw data from CSV, database, or object storage | Data volume increases |
| Preprocessing service | Cleaning, normalization, feature engineering | Data complexity increases |
| EDA service | Univariate and bivariate analysis | Ad-hoc, scales with analyst demand |
| Model training service | Build and evaluate the model | Training frequency or data size |
| Model inference service | Load the model and serve predictions | Prediction request volume |
Each of these can be an independently deployable microservice communicating with others via synchronous (REST, gRPC) or asynchronous (message queue) mechanisms.
Microservices vs pipe-and-filter — a critical distinction. The pipe-and-filter pattern demonstrated earlier (section 6.2) — where filters are connected by pipes — is still a form of monolith: all filters and pipes exist within the same codebase and process. In microservices, each component is a standalone service that can be developed, deployed, and scaled independently. The communication between them uses network protocols (HTTP/REST, gRPC) or asynchronous messaging, not in-process pipes.
Think of it this way: pipe-and-filter is a conveyor belt inside one factory. Microservices are separate factories connected by trucks. The conveyor belt is faster within the factory, but you cannot add capacity to just one station without stopping the whole belt. With separate factories, you can build a bigger paint shop without touching the assembly line.
Independent scaling example. Suppose the application has a model service (version 5) and a prediction service. If the number of users grows from 10 to 1,000,000, only the prediction service needs to scale horizontally — spin up more instances behind a load balancer. The model service remains at its current scale until the model itself needs updating (from version 5 to 5.1). This targeted scaling is impossible in a monolith, where everything scales as one unit.
Scaling scenario:
- Day 1: 10 users → 1 instance of model service, 1 instance of prediction service.
- Day 100: 1,000,000 users → 1 instance of model service, 50 instances of prediction service.
- Day 150: Model update to v5.1 → deploy new model service instance (1 → 2), keep 50 prediction instances unchanged.
In a monolith, the Day 100 scaling would require 50 instances of everything — model loading, preprocessing, UI — even though only prediction needed more capacity.
Microservices ML demo — the Iris application refactored
The professor refactored the monolithic Iris application (section 6.6) into three microservices:
1. API Gateway (port 8000): Receives user input, routes requests to the appropriate service. It does only routing — no business logic. The gateway is the single entry point; users never call downstream services directly.
2. Model Service (port 8001): Loads the logistic regression pickle model, accepts feature
lists via POST, runs model.predict(), and returns the prediction (0, 1, or 2). This service owns
the model — it is the only service that knows about the model file, the feature order, or the prediction logic.
3. Logging Service (port 8002): Receives both the input features and the model's output prediction, and logs them. In the demo, this appends to a list; in production, this would write to a database (PostgreSQL, Elasticsearch, or a cloud logging service).
Worked example — a complete prediction flow through microservices:
- The user sends
POST localhost:8000/predictwith{"features": [5.1, 3.5, 1.4, 0.2]}. - The API Gateway receives the request. It does not know how to predict — it only knows
where to forward: the model service at
localhost:8001. - The gateway forwards the features to the Model Service at
POST localhost:8001/predict. - The Model Service runs
model.predict([[5.1, 3.5, 1.4, 0.2]])and returns{"prediction": 0}. - The gateway receives the prediction. It then forwards both the input and the output to the Logging
Service at
POST localhost:8002/log. - The Logging Service stores
{features: [5.1, 3.5, 1.4, 0.2], prediction: 0}. - The gateway returns
{"prediction": 0}to the user.
The user sees the same result as the monolith, but the computation was distributed across three independent processes.
Deployment order matters. The model service and logging service must be running before the API gateway starts, because the gateway depends on both downstream services. If the gateway starts first and receives a request, it will fail because the model service is not yet available.
This is a common operational concern in microservices: service dependencies. In production, tools like Docker Compose, Kubernetes, or service meshes handle startup ordering and health checks. The demo manages this manually by starting services in the right order.
Common pitfall — the API gateway doing business logic. The professor emphasized that the API gateway should only route requests. If you put business logic (like data validation, transformation, or caching) in the gateway, it becomes a bottleneck and a single point of failure. Keep the gateway thin — it routes, and nothing else.
The microservices ML pattern trades simplicity for flexibility. Compared to the monolithic Iris app (section 6.6), this refactored version has more moving parts — three processes to start, three ports to manage, network calls instead of function calls. But it gains independent scaling (scale prediction without scaling model loading), independent deployment (update the model without touching the gateway), and fault isolation (a logging failure does not break predictions). The trade-off is worth it when these properties matter.
Real-world connection. Production ML systems at companies like Uber (Michelangelo), Netflix, and Spotify follow this pattern: a model registry stores trained models, a model service loads and serves them, a feature service provides real-time features, and an API gateway routes requests. The Iris demo is a simplified version of this architecture.
6.10 Event-Driven Architecture
6.10.1 Publish-Subscribe and Message Brokers
Why should a user wait for a log entry they will never see? In the microservices demo (section 6.9), the API gateway called the logging service synchronously — the user's request blocked until the log was written. But logging is a non-critical, fire-and-forget operation. The user does not care whether the log entry exists. This mismatch between synchronous communication and asynchronous need is what event-driven architecture solves.
Event-driven architecture decouples producers from consumers through a message broker. A producer publishes an event; any number of consumers subscribe to and process that event asynchronously. Producers and consumers are completely agnostic of each other — they know only the event topic or queue.
The textbook (T1 Ch08) defines this pattern: "Individual system components listen to messages broadcasted by other components, typically through some message bus. Since the component publishing a message does not need to know who consumes it, this architecture strongly decouples components in a system and makes it easy to add new components."
Analogy — a newspaper publisher and subscribers. A newspaper publisher prints a newspaper and drops it at newsstands. The publisher does not know who buys it — a student, a retiree, a business analyst. Each subscriber reads it for their own purpose: the student studies current events, the retiree does the crossword, the analyst tracks market news. New subscribers can appear without the publisher changing anything. The newsstand (message broker) ensures every subscriber gets their copy. This is publish-subscribe: producers publish events, consumers subscribe and process them independently.
What is an event? An event is a fact about something that happened in the system. Examples:
- A user logs in, logs out, places an order, cancels an order.
- A new product is launched.
- A model detects data drift.
- A training job completes.
Each event carries relevant data — for an order-placed event, the items and quantities. For a model-drift-detected event, the drift metric and the affected model version. Events can trigger mass notifications (emails to a million users, SMS, WhatsApp messages), and asynchronous processing avoids the latency of waiting for each downstream action.
Publish-subscribe model. The professor explained the pub-sub model with these properties:
- A set of producers sends events to a message platform.
- A set of consumers subscribes to event topics and receives events as they arrive.
- The relationship is flexible: one producer to one consumer, one to many, many to one, or many to many.
- New consumers can be added without modifying producers, and vice versa. This is the key extensibility benefit.
- The message platform (broker) guarantees reliable delivery, message ordering, and persistence until all consumers have processed each event.
Worked example — the IoT sensor scenario:
Temperature sensors distributed across Delhi publish average temperature readings every hour. Three independent systems consume these readings:
- Real-time dashboard — displays the current temperature on a public website.
- Data warehouse — stores historical readings for analysis.
- Alerting system — sends SMS alerts if temperature exceeds 45°C.
The sensors do not know who consumes the data. The dashboard team can add a new visualization without touching the sensors or the alerting system. The alerting team can add a new threshold rule without touching the sensors or the dashboard. A new consumer — say, an air quality correlation service — can subscribe to the same topic without modifying any existing component.
This is the power of event-driven architecture: adding a new consumer requires zero changes to the producer.
Message brokers. The broker sits between publishers and subscribers, ensuring three guarantees:
- Reliable delivery — no events are lost. The broker persists events until all subscribers have processed them.
- Ordering — a morning 25°C reading arrives before the afternoon 32°C reading. Events are delivered in the order they were published.
- Persistence — events are held until consumed by all subscribers; after that, they can be archived for replay or audit.
Popular message brokers:
| Broker | Type | Best for |
|---|---|---|
| Apache Kafka | Distributed log | High throughput, large-scale distributed systems, event streaming |
| RabbitMQ | Traditional message queue | General-purpose messaging, lightweight, easy setup |
| Apache MQ | Open-source broker | Legacy systems, JMS-based applications |
| Amazon SQS | Cloud-managed (AWS) | AWS-native applications, simple queue semantics |
| Azure Service Bus | Cloud-managed (Azure) | Azure-native applications, enterprise messaging |
| Google Pub/Sub | Cloud-managed (GCP) | GCP-native applications, global distribution |
Kafka is dominant in both software engineering and ML systems, especially when high throughput and scalability are required. RabbitMQ is widely used for general-purpose messaging needs. Cloud-managed services are convenient when the application already runs in that cloud.
Event-driven architecture in ML. Logging is the canonical ML use case for asynchronous communication, but there are many others:
- Model monitoring → retraining trigger: A monitoring service detects data drift (input distribution changed) and publishes a "drift-detected" event. The training service subscribes and automatically starts retraining.
- Pipeline orchestration: Data ingestion service publishes "ingestion-complete" event → preprocessing service subscribes and starts cleaning. Preprocessing publishes "features-ready" → training service starts.
- Batch inference notification: A batch prediction job completes and publishes a "batch-complete" event → downstream services (dashboards, databases, notification systems) consume independently.
The architect's design decision. The professor highlighted an important design refinement that a student identified during the demo.
In the event-driven version of the demo, the API gateway published log events to the message broker. But this violates the gateway's design principle: an API gateway should only route, not perform business logic like logging.
The correct architectural pattern: The model service itself, after producing a prediction, should publish the input and output as an event directly to the message broker. The logging service consumes from the broker asynchronously. This offloads the gateway and keeps each service's responsibility clean.
The demo placed the logging logic in the gateway only to minimize code changes for instructional clarity, but in production, the event should originate from the service that owns the data (the model service), not the gateway.
This is a recurring theme in software architecture: the demo optimizes for teaching clarity, while production systems optimize for correctness. Recognizing this gap — and knowing which design is actually correct — is a key architectural skill.
Choosing a message broker — decision factors:
- Open source vs commercial: RabbitMQ and Kafka are open-source; cloud-managed services are commercial (pay per message).
- Cloud alignment: If your stack is on AWS, SQS is a natural choice; on Azure, Azure Service Bus; on GCP, Google Pub/Sub.
- Distributed and scalable: Kafka excels in large-scale distributed systems with multiple nodes and high throughput requirements.
- On-premise deployments: Both Kafka and RabbitMQ work well; the choice depends on throughput and ordering requirements.
For most ML systems, Kafka is the default choice when scale matters, and RabbitMQ is the default when simplicity matters.
Real-world connection. Event-driven architecture is the backbone of real-time ML systems. LinkedIn uses Kafka to process over 7 trillion messages per day, including ML feature pipelines. Uber uses event-driven architecture for real-time pricing, driver matching, and fraud detection. The pattern is essential whenever ML models need to react to events in real-time rather than processing batches on a schedule.
6.11 Model Registry Pattern and Version Control
6.11.1 Model Registry and Version Control Systems
How do you track which model is running in production, what data it was trained on, and who approved it? In a monolith with one model, you might keep track mentally. But when you have 50 models across 10 services, each updated weekly, mental tracking fails. The model registry pattern solves this by treating models as first-class versioned artifacts — just like code.
The registry pattern — well-established in software engineering — provides a central hub where artifacts (code, binaries, models) are stored and versioned. A package registry (like npm, PyPI) stores code packages. A container registry (like Docker Hub) stores container images. In ML systems, the model registry pattern extends this concept: a centralized repository for tracking, versioning, managing, and governing ML models throughout their lifecycle.
Analogy — a library's catalog system. A library does not just pile books on shelves. Each book has a catalog entry: title, author, edition, publication date, location on the shelf, who borrowed it last, and whether it has been reviewed by the librarian. A model registry is the catalog system for ML models — it does not just store the model file, but tracks everything about it: version, training data, metrics, deployment status, and who approved it.
Core capabilities of a model registry:
| Capability | What it provides |
|---|---|
| Versioning | Every model iteration gets a unique version identifier (e.g., iris-v5.1), capturing the
code, hyperparameters, and training data snapshot that produced it. You can always answer: "What changed
between v5.0 and v5.1?" |
| Storage and persistence | The model artifact itself (the .pkl, .h5, .onnx file) is stored
and retrievable. You can download any version at any time. |
| Metadata management | Training metrics (accuracy, F1, loss), evaluation results, deployment status (staging, production, archived), and provenance information (who trained it, when, on what data) are attached to each version. |
| Governance | Access control (who can promote a model to production), approval workflows (requires sign-off from ML lead before deployment), and compliance tracking (audit trail for regulated industries). |
Popular model registry tools:
| Tool | Type | Key strength |
|---|---|---|
| MLflow | Open-source | Widely adopted, integrates with many ML frameworks (scikit-learn, PyTorch, TensorFlow), tracks experiments and models |
| DVC (Data Version Control) | Open-source | Extends Git-like versioning to data and models — stores large files in S3/GCS while keeping metadata in Git |
| Weights & Biases | Commercial (free tier) | Popular for experiment tracking, visualization, and model management; strong team collaboration features |
| Amazon SageMaker Model Registry | Cloud-managed | Cloud-native on AWS; integrates with SageMaker training and deployment pipelines |
The professor noted that DVC will be covered in a dedicated webinar, and MLflow model registry will be demonstrated in the next class.
Version control evolution — three generations. The professor traced the history of version control to explain why ML needs specialized tools:
Generation 1 — File-system based (1970s–80s):
Tools like RCS (Revision Control System) and SCCS (Source Code Control
System) allowed versioning one file at a time. You could check in a new version of main.c and
retrieve any previous version. But there was no concept of multi-file operations — you could not atomically
commit changes to main.c and utils.c together. These tools are now obsolete.
Generation 2 — Centralized (1990s–2000s):
Tools like CVS (Concurrent Versions System) and Subversion (SVN) supported multi-file operations. All files lived in a central repository on a server. Developers checked out files, made changes, and committed back to the central repo. The limitation: if the central server went down, no one could commit. And every operation required network access.
Generation 3 — Distributed (2005–present):
Git. Every developer has a complete local copy of the entire repository, including full history. The workflow: pull from a remote (GitHub, GitLab) to local, make changes and commit locally (potentially many commits, even offline), then push to the shared remote when ready. Git is the dominant version control system today — used by virtually every software team worldwide.
Why ML needs multiple tracking dimensions. This is the key architectural insight of this section.
Unlike pure software, ML systems involve multiple artifacts that change independently:
| Artifact | Changes when... | Versioned by... |
|---|---|---|
| Code | Bug fix, feature addition | Git |
| Datasets | New data collected, cleaning fixed | DVC |
| Model weights | Retrained, fine-tuned | MLflow, model registry |
| Hyperparameters | Experiment with different settings | MLflow, W&B |
| Experiment configs | Different preprocessing, features | MLflow, W&B |
Git alone tracks code well but struggles with large datasets (gigabytes of training data) and model binaries (hundreds of megabytes). Storing a 500MB model file in Git is technically possible but bloats the repository and slows every operation. This is why specialized tools like DVC, MLflow, and Weights & Biases have emerged — they complement Git by handling the data and model dimensions of versioning.
The model registry is the Git of ML models. Just as Git tracks every change to code (who changed what, when, and why), the model registry tracks every change to models (who retrained it, on what data, with what metrics). The difference is that Git works on text files (diffs, merges), while model registries work on large binary artifacts (model files, datasets) that need specialized storage and metadata.
Common pitfall — "we have Git, we don't need a model registry." Git tracks code. It does not track which dataset version produced which model version, what the training metrics were, or whether the model has been approved for production. Without a model registry, answering "which model is running in production and why?" requires manual investigation — asking around, checking deployment scripts, reading Slack messages. A model registry makes this information instantly available.
Real-world connection. MLflow is the most widely adopted open-source model registry, used by companies like Databricks, Microsoft, and thousands of ML teams. DVC is popular in teams that want Git-like workflows for data and models. Weights & Biases dominates experiment tracking in research and industry labs. Amazon SageMaker Model Registry is the natural choice for teams already on AWS. The choice depends on your stack, team size, and whether you prefer open-source or managed services.
Exam Guidance Summary
Assignment 1 — Implement the RAJ architecture. Be creative — choose a good problem statement that interests your team. Work actively in your four-member groups. Apply the architectural patterns covered in class: CQRS for separating ingestion and query, pipe-and-filter for the data flow, and consider whether event-driven architecture makes sense for any part of your system. A well-executed assignment can serve as the foundation for your dissertation.
Model choice — any LLM is acceptable. You may use any LLM — API-based (OpenAI GPT-4o mini, Gemini) or locally downloaded (Mistral 7B, Gemma 2B). Both options are acceptable and encouraged for experimentation. Smaller local models work well for small document sets; API-based models are better for larger corpora. The architectural pattern is the same regardless of which model you choose.
Course materials. All code, PPTs, and lab materials are uploaded on the Taxila portal under the labs and PPTs folders. Reference them while working on assignments.
Course scope notes:
- REST is the primary communication protocol covered. gRPC and GraphQL are out of scope for this course but may be explored independently.
- Docker and Kubernetes will be covered post-midterm.
- The architectures covered so far — pipe-and-filter, CQRS, RAJ, monolithic, microservices, event-driven — form the core architectural curriculum.
- The model registry pattern begins the design-pattern portion of the course.
Upcoming webinars: DVC (evolution of version control for ML) will be covered in a scheduled webinar. MLflow model registry will be demonstrated in the next class.
Key Industry Applications
This lecture referenced numerous companies and tools. Below is a consolidated reference of where each appears in the architectural landscape.
Microservices and deployment at scale:
- Amazon: Approximately 23,000 microservices deployments per day; e-commerce and AWS services use microservices and event-driven architectures.
- Google: Approximately 5,500 microservices deployments per day; gRPC originated at Google.
- Flipkart (2014): Big Billion Day failure attributed to monolithic architecture that could not scale under peak load — the canonical example of monolithic scalability limits.
- Hotstar (2019): Scaled to 25 million concurrent users during an India vs New Zealand cricket match, enabled by microservices architecture.
- YouTube: Uses microservices — recommendation service can fail independently without affecting video playback, comments, or downloads.
- Instagram: Built with Python, React Native, and JavaScript; polyglot microservices architecture.
- Swiggy/Zomato: Food delivery platforms — the professor's archetypal example for comparing monolithic and microservices architectures.
RAJ ecosystem:
- OpenAI: GPT-4o mini and text-embedding-3-small used in the RAJ demo; multimodal LLMs enable image-to-text pipelines.
- LangChain: Modular framework for building RAJ pipelines with pluggable document loaders, text splitters, embedding models, and vector stores.
- ChromaDB: Vector database for storing and querying embeddings in the RAJ architecture.
- Streamlit / Gradio: Lightweight UI frameworks for ML application prototyping and demo interfaces.
ML serving:
- FastAPI + Uvicorn: De facto standard for ML serving in Python; used throughout all architecture demonstrations.
Event-driven infrastructure:
- Apache Kafka: Dominant message broker for event-driven architectures in both software and ML systems.
- RabbitMQ: Widely used open-source message broker using the AMQP protocol.
- IoT sensors + event-driven: Environmental monitoring systems where sensors publish readings as events consumed by dashboards, warehouses, and alerting systems.
Model management:
- MLflow: Open-source model registry and experiment tracking tool.
- DVC: Data and model version control tool extending Git-like workflows to ML artifacts.
- Weights & Biases: Experiment tracking and model management platform.
- Amazon SageMaker: Cloud-native ML platform with built-in model registry.
SEML Lecture 6 Notes · Monolith, Microservices, Event-Driven Architecture, and Model Registry
Sections Breakdown
Two strategies for handling image input in RAJ: OCR and multimodal LLM
CQRS separates write and read pipelines in RAJ architecture
Live demo of end-to-end RAJ pipeline with real course materials
Student questions on RAJ architecture and semantic search
Software engineering foundations of monolithic architecture
ML-specific monolithic pattern with Iris classification demo
Microservices principles, industry adoption, and scaling benefits
Comparison of API protocols for microservices
Refactoring monolithic ML into microservices
Publish-subscribe model and message brokers
Model registry pattern and ML-specific version control tools
Assignment guidance and course scope notes
Consolidated reference of companies and tools
Exam Revision Notes
Below is the distilled, exam-ready core. Every entry comes from the full explanation above. Use this section for rapid review; return to the main notes when a point needs more context.
6.1 RAJ Architecture with Multimodal Input
Must-know: Two multimodal RAJ strategies: OCR then text pipeline vs multimodal LLM direct. Both converge to the same downstream pipeline — only the pre-processing stage changes.
⚠️ Top pitfall: Assuming OCR output quality matches the original document. OCR introduces errors and loses visual structure.
Self-check: What is the key architectural insight about handling images in RAJ?
Connects to: 6.2, 6.3
6.2 Implementing RAJ with CQRS and Pipe-and-Filter
Must-know: CQRS separates write (ingest.py: load, split, embed, store) from read (app.py: prompt, embed, retrieve, generate). No LLM in ingestion pipeline. Same embedding model must be used on both sides.
⚠️ Top pitfall: Using different embedding models on write and read sides — vectors live in different spaces, similarity search fails.
Self-check: Why does the RAJ ingestion pipeline not use an LLM?
Connects to: 6.1, 6.3, 6.4
6.3 RAJ Demo: Document Ingestion and Query Pipeline
Must-know: RAJ demo: 187 pages → 183 chunks. System refuses out-of-context queries (no hallucination). Source citations allow verification of grounding.
⚠️ Top pitfall: Mixing API and local models without adjusting chunk count — smaller models struggle with long retrieved context.
Self-check: What happens when you ask the RAJ system a question about weather?
Connects to: 6.2, 6.4
6.4 RAJ Student Q&A
Must-know: LLM in RAJ is a language processor, not a knowledge source. Embeddings enable semantic search (conceptual matches, not just keyword matches). Agentic RAJ adds decision-making layers when retrieved context is insufficient.
⚠️ Top pitfall: Confusing the LLM's role in RAJ (constrained to retrieved context) with standalone chat (draws on parametric knowledge).
Self-check: What is the difference between keyword search and semantic search in RAJ?
Connects to: 6.1, 6.2, 6.3
6.5 Monolithic Architecture
Must-know: Monolith: one codebase, one deploy, one DB. Cannot scale modules independently. Flipkart 2014 failure (could not handle traffic surge). Twitter migrated from monolith to microservices for scalability.
⚠️ Top pitfall: Assuming monolithic means bad. For small teams and prototypes, monoliths are the right choice. Problems emerge at scale.
Self-check: What was the cause of the Flipkart Big Billion Day failure in 2014?
Connects to: 6.6, 6.7
6.6 Monolithic ML Pattern and Iris Demo
Must-know: Monolithic ML: one process for model loading + prediction + serving. FastAPI + Uvicorn is the standard Python stack. Cannot scale inference independently from the rest of the app.
⚠️ Top pitfall: Model updates require full redeployment in monolithic ML — even a minor version change redeploys everything.
Self-check: What does the Iris classification demo return for input [5.1, 3.5, 1.4, 0.2]?
Connects to: 6.5, 6.7, 6.9
6.7 Microservices Architecture
Must-know: Microservices: SRP, each service has own DB, API gateway routes requests. Amazon 23K deployments/day. Independent scalability is the key advantage. Watch out for distributed monolith anti-pattern.
⚠️ Top pitfall: Distributed monolith — microservices that are tightly coupled, share databases, or must be deployed together. You get the complexity of distributed systems without the benefits.
Self-check: Why can Amazon do 23,000 deployments per day?
Connects to: 6.5, 6.8, 6.9, 6.10
6.8 Synchronous Communication: REST, GraphQL, and gRPC
Must-know: REST: HTTP/JSON, ~90% ML apps, FastAPI standard. GraphQL: single request for nested data, social media. gRPC: binary Protobuf + HTTP/2, fastest, requires .proto contract. Know trade-offs.
⚠️ Top pitfall: Choosing gRPC for external APIs without considering that clients must implement the Protobuf contract — REST is simpler for public APIs.
Self-check: Why is gRPC faster than REST?
Connects to: 6.7, 6.9, 6.10
6.9 Microservices ML Pattern and Demo
Must-know: Microservices ML: API Gateway (routing), Model Service (inference), Logging Service (audit). Deployment order matters — downstream services must start before gateway. Gateway should only route, no business logic.
⚠️ Top pitfall: Putting business logic in the API gateway — it should only route requests, nothing else.
Self-check: Why must the model service start before the API gateway?
Connects to: 6.6, 6.7, 6.10
6.10 Event-Driven Architecture
Must-know: Event-driven: producers publish, consumers subscribe via broker. Broker guarantees: reliable delivery, ordering, persistence. Model service should publish events, not the API gateway. Kafka for scale, RabbitMQ for simplicity.
⚠️ Top pitfall: Placing event-publishing logic in the API gateway — the model service should publish events directly to the broker.
Self-check: What three guarantees does a message broker provide?
Connects to: 6.7, 6.9, 6.11
6.11 Model Registry Pattern and Version Control
Must-know: Model registry: version, store, manage, govern ML models. Version control evolution: RCS/SCCS → CVS/SVN → Git. ML needs specialized tools (DVC, MLflow, W&B) because Git cannot handle large data/model binaries. Three generations of version control.
⚠️ Top pitfall: Assuming Git is sufficient for ML versioning — it tracks code but not datasets, model weights, or training metrics.
Self-check: Why can't Git alone handle ML model versioning?
Connects to: 6.7, 6.10
Was this lecture useful?
BitsNotes AI Assistant
Subject Notes AssistantConfigure AI Chat
Choose how to access the chatbotSigned in as
Powered by BitsNotes — 20 messages per day. No API key needed. Want unlimited access? Use "Bring Your Own Key" mode.
Sign in to use AI Chat
Get 20 free AI messages per day to ask questions about your lecture notes. Sign in with Google or GitHub — it takes 5 seconds.
Sign In to BitsNotesSwitch to "Bring Your Own Key" tab above for unlimited access with any OpenAI-compatible provider.