<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Code Engineering]]></title><description><![CDATA[Code Engineering explores how code shapes systems and how systems, in turn, shape code. A space where code is both the foundation of what we build and the key t]]></description><link>https://jackcoder.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Tue, 01 Sep 2026 00:27:53 GMT</lastBuildDate><atom:link href="https://jackcoder.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Understand LLM Context]]></title><description><![CDATA[Concepts
Fundamentals and Representation
At its core, an LLM is stateless. Every API call begins with a blank slate, and the model retains nothing from previous interactions unless explicitly provided. What we call context is essentially the working ...]]></description><link>https://jackcoder.hashnode.dev/understand-llm-context</link><guid isPermaLink="true">https://jackcoder.hashnode.dev/understand-llm-context</guid><category><![CDATA[llm]]></category><category><![CDATA[context]]></category><category><![CDATA[AI]]></category><category><![CDATA[RAG ]]></category><category><![CDATA[mcp]]></category><dc:creator><![CDATA[Giacomo Stelluti Scala]]></dc:creator><pubDate>Fri, 09 Jan 2026 12:15:43 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1767960747201/5e0fcaf8-700d-4413-9bdc-7cc89665e57e.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-concepts">Concepts</h2>
<h3 id="heading-fundamentals-and-representation">Fundamentals and Representation</h3>
<p>At its core, an LLM is stateless. Every API call begins with a blank slate, and the model retains nothing from previous interactions unless explicitly provided. What we call <em>context</em> is essentially the working memory of the system: the full payload of text (and increasingly, other modalities) that gets passed to the model on each inference call. This is distinct from persistent memory or long-term storage; context exists only for the duration of a single forward pass.</p>
<p>Understanding this distinction is critical for system design. When users perceive a chatbot as "remembering" their preferences, that illusion is constructed entirely by the application layer. The model itself has no notion of yesterday's conversation. Every piece of relevant history must be serialized and injected into the context window, every single time.</p>
<p>Before the model can process text, it must be converted into <strong>tokens</strong>. Tokenization algorithms like <strong>Byte Pair Encoding</strong> (BPE) or <strong>SentencePiece</strong> break text into subword units that the model can understand. A crucial insight for engineers: tokens do not map cleanly to words. The phrase "unbelievable" might be a single token, while ChatGPT could be split into three. This has direct implications for context budgeting. When you have a 128K token limit, that does not translate to 128,000 words. Depending on the language, formatting, and content type, you might fit anywhere from 80K to 300K words. Technical content with code snippets, special characters, or non-English text tends to be less token-efficient.</p>
<h3 id="heading-attention-mechanism">Attention Mechanism</h3>
<p>Context windows exist because of how transformer models process information. The self-attention mechanism allows every token in the input to <em>attend</em> to every other token, computing relevance scores and building contextual representations. This is what enables LLMs to understand that "it" in sentence five refers to an object mentioned in sentence one.</p>
<p>However, this architecture comes with constraints. The attention computation scales with the square of the sequence length, which is why extending context windows has historically been so challenging. A model processing 8K tokens performs roughly 64 million attention calculations. Scale that to 128K tokens and you're looking at over 16 billion. This computational reality shapes everything from latency to infrastructure costs, and understanding it helps explain why "just make the context bigger" is never a simple solution.</p>
<h2 id="heading-context-window-amp-its-management">Context Window &amp; Its Management</h2>
<h3 id="heading-anatomy-of-a-context-window">Anatomy of a Context Window</h3>
<p>Every model has a hard limit on <strong>context size</strong>, defined by its architecture and training. But the effective limit is usually smaller. System prompts, safety guidelines, and formatting overhead consume tokens before the user's content ever arrives. In production systems, it's common to reserve 10-20% of the context window for system-level instructions, leaving the remainder for actual conversation and retrieved content.</p>
<p>Context windows have grown dramatically over the past two years. GPT-3 launched with 4K tokens. Today, models routinely support 128K, 200K, or even 1M+ tokens. <strong>But bigger is not automatically better</strong>. Larger windows introduce latency, increase costs, and as we'll explore later, can actually degrade output quality in subtle ways. The evolution of window sizes reflects both architectural innovation and market pressure, but engineers should resist the temptation to treat expanded limits as a solution to all context problems.</p>
<p><strong>The system prompt deserves special attention</strong>. This is the foundational instruction set that shapes model behavior, and it sits at the privileged beginning of the context. In complex applications, system prompts can run to thousands of tokens. Every token spent here is unavailable for conversation history or retrieved documents. Designing efficient, effective system prompts is an underappreciated skill in LLM engineering.</p>
<h3 id="heading-composition-and-truncation-strategies">Composition and Truncation Strategies</h3>
<p>Production systems rarely dump content into the context window haphazardly. Instead, they implement layered architectures with clear priority hierarchies. A typical structure might place system instructions first, followed by relevant retrieved documents, then conversation history, and finally the current user query. Each layer has a token budget, and when the total exceeds the limit, something must be cut.</p>
<p><strong>The simplest truncation strategy is FIFO</strong>: first in, first out. When context overflows, drop the oldest messages. This is easy to implement but problematic in practice. Conversations often reference earlier content, and blindly removing old messages can sever important threads. Users might ask "what did I say about the budget?" when that budget discussion was truncated three turns ago.</p>
<p><strong>Semantic-aware truncation</strong> offers a more sophisticated approach. Rather than purely chronological removal, the system scores content by relevance to the current query and retains what matters most. This requires additional computation (typically embedding similarity) but preserves coherence better. Some systems maintain "pinned" content that never gets truncated: critical user preferences, key facts, or summary checkpoints that anchor the conversation's context regardless of length.</p>
<p><strong>Token budgeting can be fixed or dynamic</strong>. Fixed allocation assigns rigid limits to each context layer (e.g., 4K for system prompt, 20K for documents, 10K for history). Dynamic budgeting adjusts based on the specific query; a research question might allocate more to retrieved documents, while a casual chat prioritizes conversation history. The right approach depends on your application's needs and how predictable user interactions are.</p>
<h3 id="heading-multi-turn-session-state">Multi-Turn Session State</h3>
<p>From the API perspective, <strong>every call is independent</strong>. But users experience conversations as continuous sessions. Bridging this gap is the application's responsibility. The client (or an intermediary service) must store conversation history and reconstruct it for each request.</p>
<p>Session serialization raises practical questions. How do you store conversation state? How do you handle concurrent requests within the same session? When a user returns after hours or days, do you restore the full history or start fresh? These decisions affect user experience, cost, and system complexity. Some architectures maintain server-side session stores with TTLs, while others push state management entirely to the client. There is no universal answer, only trade-offs appropriate to your use case.</p>
<h2 id="heading-context-rot">Context Rot</h2>
<h3 id="heading-attention-degradation-and-semantic-drift">Attention Degradation and Semantic Drift</h3>
<p>Larger context windows create an illusion of unlimited memory, but attention is not evenly distributed. Research has documented the <em>Lost in the Middle</em> phenomenon: information placed in the middle of long contexts is retrieved and utilized less reliably than content at the beginning or end. Models exhibit both primacy bias (favoring early content) and recency bias (favoring recent content), leaving the middle as a kind of dead zone.</p>
<p>This has direct engineering implications. If you stuff 50 documents into context, the model might effectively ignore 30 of them based purely on position. Relevance ranking becomes crucial. The most important content should be placed strategically, not just appended sequentially.</p>
<p>As conversations extend, a subtler problem emerges: semantic drift. Initial instructions get diluted as the context fills with other content. A model told in the system prompt to "respond formally" might gradually shift to casual language as informal user messages accumulate. Conflicting information compounds the issue. If a user corrects themselves multiple times, all versions persist in context. The model must somehow reconcile contradictions, and it doesn't always do so reliably. In extreme cases, extended sessions can exhibit <strong>persona collapse</strong>, where the model's coherent behavior degrades into inconsistency.</p>
<h3 id="heading-compaction-challenges">Compaction Challenges</h3>
<p>The obvious solution to context limits is summarization: periodically condense history to free up space. But <strong>summarization is inherently lossy</strong>. Details vanish. Nuance flattens. A summary stating "the user discussed budget concerns" loses the specific numbers, the emotional tone, and the context of why those concerns arose.</p>
<p>Reference resolution becomes particularly fragile after compaction. If the original context contained "I'll call my brother Mike about this," a summary might reduce this to "user will follow up with family." When the user later asks "did I mention Mike?", the model has no answer. Proper nouns, specific commitments, and temporal references are especially vulnerable to summarization loss.</p>
<p>Perhaps most insidious is the summarization-of-summaries problem. As conversations stretch across many sessions, you might summarize a summary, then summarize that summary again. <strong>Each pass compounds information loss</strong>. After several iterations, the resulting context may bear little resemblance to what actually occurred. Designing compaction strategies that preserve essential facts while discarding redundancy is a genuinely hard problem with no clean solutions.</p>
<h3 id="heading-computational-and-latency-costs">Computational and Latency Costs</h3>
<p>Context size directly impacts performance. Time-to-first-token (TTFT), the delay before the model begins streaming a response, increases with context length. For latency-sensitive applications like chat interfaces, this degradation is noticeable. Users waiting 3-5 seconds for a response in a long conversation will feel the friction.</p>
<p>Cost scales similarly. Most API pricing is based on token count, both input and output. A conversation that has accumulated 80K tokens of context costs 20 times more per turn than a fresh conversation with 4K. Over thousands of users and millions of requests, this adds up quickly. <strong>Context management is not just an engineering problem; it's an economic one</strong>.</p>
<p>Modern inference infrastructure uses KV-caching to avoid recomputing attention for unchanged context. But this cache is fragile. Any modification to context (even inserting a single token) can invalidate the cache and force full recomputation. Systems that dynamically inject retrieved content or reorder context elements may inadvertently defeat caching optimizations, paying the full computational cost on every request.</p>
<h2 id="heading-current-approaches-and-future-strategies">Current Approaches and Future Strategies</h2>
<h3 id="heading-retrieval-augmented-generation-rag">Retrieval-Augmented Generation (RAG)</h3>
<p>RAG addresses context limitations by <strong>decoupling storage from the context window</strong>. Instead of cramming everything into context, you store information externally (typically in a vector database) and retrieve only what's relevant for each query.</p>
<p>The approach involves chunking documents into segments, generating embeddings for each chunk, and performing similarity search at query time. Effective chunking is harder than it sounds. Chunks that are too small lose context; chunks too large waste tokens and reduce precision. Hybrid search combining semantic similarity with keyword matching often outperforms pure vector search, especially for queries involving specific names, codes, or technical terms.</p>
<p>RAG is not a silver bullet. Relevance scoring is imperfect, and important information gets missed. There's also a tension between relevance and recency. A semantically similar document from two years ago might outrank a less similar but current document. For conversational applications, RAG struggles with the inherently temporal nature of dialogue. "What did we discuss yesterday?" requires temporal awareness that pure similarity search doesn't provide.</p>
<h3 id="heading-memory-architectures-and-compression-techniques">Memory Architectures and Compression Techniques</h3>
<p>Sophisticated systems implement tiered memory architectures inspired by <strong>cognitive science</strong>. Working memory holds the current conversation and immediate context. Episodic memory stores summaries of past interactions, tagged with temporal metadata. Semantic memory captures distilled facts and user preferences that persist across sessions. Each tier has different retention policies, summarization strategies, and retrieval mechanisms.</p>
<p>Summarization pipelines in these architectures must preserve metadata alongside content. A summary should retain timestamps, confidence levels, and source references so the system can trace back to originals when needed. Event-driven consolidation (summarizing after significant interactions) often works better than purely time-based approaches, as it aligns with natural conversation boundaries.</p>
<p>Prompt compression techniques like LLMLingua and AutoCompressors offer a different angle. Rather than summarizing at the semantic level, these approaches <strong>compress text while preserving essential meaning</strong>. The model receives a compressed representation that maintains more information than a natural language summary of equivalent length. This is still an active research area with trade-offs between compression ratio and fidelity.</p>
<p>The fundamental tension in all compression approaches is lossy versus lossless. Lossless preservation of full context is eventually impossible under fixed token limits. Lossy approaches sacrifice information. The art is in choosing what to sacrifice, and no automated system does this perfectly. Human-in-the-loop review of memory consolidation remains valuable for high-stakes applications.</p>
<h3 id="heading-architectural-innovations-and-future-outlook">Architectural Innovations and Future Outlook</h3>
<p>Architectural research continues to push at context limitations. <strong>Sparse attention patterns</strong>, implemented in models like Longformer and BigBird, reduce computational complexity by limiting which tokens attend to which. Rather than full quadratic attention, these models use <strong>sliding windows combined with global tokens</strong>, allowing much longer contexts without proportional cost increases.</p>
<p>State-space models like Mamba represent a more radical departure. By replacing attention with recurrent state-space layers, these architectures achieve linear scaling with sequence length. Early results are promising, though the approach involves different trade-offs around in-context learning and fine-tuning behavior. Whether state-space models will complement or replace transformers for long-context applications remains an open question.</p>
<p>Tool-augmented context offers a pragmatic near-term solution. Instead of storing everything in context, the model gains access to external memory through tool calls. It can query a database, search past conversations, or retrieve specific documents on demand. This shifts <strong>context management from passive accumulation to active retrieval</strong>, aligning compute with actual information needs.</p>
<p><strong>Self-reflective context pruning</strong> represents an emerging pattern where the model itself evaluates context relevance. Rather than relying on external heuristics, the LLM scores which parts of its context are most valuable for the current task and suggests what can be safely removed. This approach leverages the model's understanding of its own attention patterns but requires careful prompt engineering to avoid runaway pruning.</p>
<p>The trajectory of these developments points toward what researchers increasingly call <strong>cognitive architectures</strong>. These are integrated systems combining working memory, long-term storage, retrieval, tool use, and meta-cognitive monitoring into coherent wholes. The context window becomes just one component in a larger memory infrastructure, dynamically managed rather than passively filled. Production systems are already moving in this direction, and the <strong>next generation of foundation models will likely incorporate memory primitives more natively</strong>.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Context is the central bottleneck in LLM system design. Every architectural decision, from tokenization to summarization, from retrieval to caching, ultimately flows through this constraint. <strong>Engineers who understand context deeply build better systems</strong>.</p>
<p>The trade-offs are persistent: capacity versus fidelity versus cost. Larger windows offer more information but introduce latency, expense, and attention degradation. Compression preserves tokens but loses nuance. Retrieval enables scale but imperfect relevance. There is no free lunch.</p>
<p>The practical imperative is to design context-aware abstractions from day one. Don't treat the context window as a dumping ground. Implement thoughtful layering, prioritization, and lifecycle management early. Monitor context utilization and quality metrics in production. Build for the systems of tomorrow, where context is actively managed by intelligent memory architectures, not just passively accumulated until limits force truncation.</p>
]]></content:encoded></item><item><title><![CDATA[RAG and Agentic AI]]></title><description><![CDATA[Overview
Agentic AI (multi-agent LLM workflows) and Retrieval-Augmented Generation (RAG) are complementary. Agents run perceive → plan → act → observe loops and call tools/APIs. RAG supplies curated, evidence-backed context via an ingestion and a ret...]]></description><link>https://jackcoder.hashnode.dev/rag-and-agentic-ai</link><guid isPermaLink="true">https://jackcoder.hashnode.dev/rag-and-agentic-ai</guid><category><![CDATA[AI]]></category><category><![CDATA[RAG ]]></category><category><![CDATA[Document Intelligence]]></category><category><![CDATA[agents]]></category><category><![CDATA[agentic AI]]></category><dc:creator><![CDATA[Giacomo Stelluti Scala]]></dc:creator><pubDate>Sun, 04 Jan 2026 16:49:18 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1767544994319/46c64855-20c4-4186-bccd-871ffd2ba0e9.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-overview">Overview</h2>
<p>Agentic AI (multi-agent <strong>LLM</strong> workflows) and <strong>Retrieval-Augmented Generation</strong> (RAG) are complementary. Agents run perceive → plan → act → observe loops and call tools/APIs. RAG supplies curated, evidence-backed context via an ingestion and a retrieval pipeline to reduce hallucination and improve decision accuracy.</p>
<p>This combination addresses a fundamental limitation of standalone LLMs: while they're remarkably good at reasoning and generation, their knowledge is frozen at training time. Worse, they can't reliably tell the difference between what they actually know and what they're making up. RAG grounds agent outputs in verifiable sources, which turns speculative responses into evidence-based decisions. In enterprise settings where accuracy, auditability, and domain-specific knowledge aren't optional, this matters a lot.</p>
<p>What's changed recently is how these systems relate to each other. Modern agentic systems increasingly treat RAG not as a separate preprocessing step but as an intrinsic capability. Agents autonomously decide when to retrieve, what sources to consult, and how to synthesize multiple evidence streams. This shift from <em>RAG as preprocessing</em> to <em>RAG as agent skill</em> enables more sophisticated workflows where retrieval happens dynamically based on what the task actually requires.</p>
<h2 id="heading-ingestion-pipeline">Ingestion Pipeline</h2>
<h3 id="heading-source-normalization">Source Normalization</h3>
<p>Convert PDFs, Word docs, spreadsheets, images, and other artifacts into machine-readable, LLM-friendly formats such as <strong>Markdown</strong>. Use document conversion utilities (e.g.: <strong>Docling</strong>) to normalize PDFs to Markdown while preserving structure and metadata.</p>
<p>Markdown makes sense as a target format because it preserves semantic structure (headings, lists, tables) while staying lightweight and universally parseable. Other solid options include <strong>Unstructured.io</strong> for gnarly document layouts, <strong>PyMuPDF</strong> for PDF text extraction with coordinate preservation, and <strong>Pandoc</strong> for cross-format conversions. For scanned documents, you'll need OCR engines like <strong>Tesseract</strong> or cloud services (Azure Document Intelligence, AWS Textract, etc) that can handle degraded image quality and complex layouts.</p>
<p>One thing worth setting up early: document fingerprinting using content hashes to catch duplicates and track versions. This prevents your index from bloating with redundant content and enables incremental updates when source documents change. You'll also need clear rules for handling conflicts when the same information shows up in multiple sources with different values.</p>
<h3 id="heading-content-enrichment">Content Enrichment</h3>
<p>Extract and preserve <strong>tables, graphs, captions, page numbers, image alt text</strong>; detect truncated pages and flag for <strong>OCR/manual</strong> review.</p>
<p>Tables are tricky. Simple extraction often loses the semantic relationship between headers and cells. You'll want table-aware parsing that converts tables to structured formats (JSON, CSV) with header associations preserved, or linearizes them into natural language descriptions for embedding. For complex nested tables, storing both the structured representation and a natural language summary works well.</p>
<p>Graphs and charts need special handling too. Extract the underlying data where possible, generate descriptive alt text using vision models, and store both the visual reference and textual description. Keep a mapping between extracted content and its original location (page, bounding box coordinates) so you can cite precisely and let users visually verify.</p>
<p>Set up automated quality gates that flag pages with low OCR confidence scores, detect truncated content at page boundaries, and identify missing figures or broken cross-references. Route flagged content to human review rather than silently ingesting garbage that will pollute your retrieval results downstream.</p>
<h3 id="heading-chunking-strategy">Chunking Strategy</h3>
<p>Split documents into semantically coherent chunks sized to balance retrieval relevance and LLM context limits; include chunk metadata (<strong>source id; page; offset; confidence</strong>) for provenance.</p>
<p>If I had to pick one thing that makes or breaks <strong>RAG systems</strong>, it's chunking. This is probably the highest-leverage optimization in the entire pipeline. Naive approaches (fixed character counts, sentence boundaries) fragment semantic units and create chunks that lack sufficient context to stand on their own. Semantic chunking that respects document structure, splitting at section boundaries, paragraph breaks, or topic transitions detected via embedding similarity; that works much better.</p>
<p>Chunk sizes typically range from 256 to 1024 tokens, with 512 being a reasonable default. Overlapping windows (10-20% overlap) prevent <strong>information loss</strong> at chunk boundaries, which matters because critical details often span your artificial split points. For highly structured documents like legal contracts or technical specifications, hierarchical chunking that preserves parent-child relationships between sections is worth the extra complexity.</p>
<p>Your metadata schema should capture: source document identifier, version/timestamp, page numbers, section hierarchy, extraction confidence, content type (prose, table, code, list), and any domain-specific tags. This metadata powers filtered retrieval ("only search 2024 policy documents") and makes provenance tracking possible in downstream outputs.</p>
<h3 id="heading-embedding-generation">Embedding Generation</h3>
<p>Use a single, consistent embedding model for both ingestion and query embedding to avoid embedding-space mismatch; store embeddings with chunk metadata.</p>
<p>Embedding model selection has a big impact on retrieval quality. General-purpose models (<strong>OpenAI text-embedding-3-large</strong>, <strong>Cohere embed-v3</strong>, <strong>BGE-large</strong>) work well across domains, but <strong>domain-specific fine-tuning</strong> can yield substantial gains for specialized corpora in legal, medical, or scientific contexts. Test models on your actual retrieval tasks using held-out query sets before committing to one.</p>
<p>Dimension matters for both quality and cost. Higher-dimensional embeddings (1536, 3072) capture more nuance but increase storage and search latency. Many vector databases support dimensionality reduction or quantization (product quantization, scalar quantization) to trade modest accuracy loss for significant efficiency gains at scale.</p>
<p>Here's something people often learn the hard way: implement embedding versioning from day one. When you eventually upgrade embedding models (and you will), you'll need to re-embed your entire corpus. Without version tracking, you risk mixing incompatible embedding spaces. Store the model identifier alongside each embedding vector and build migration tooling for model transitions before you need it.</p>
<h3 id="heading-vector-db-write">Vector DB write</h3>
<p>Persist embeddings and metadata to a vector database optimized for fast similarity search and re-ranking hooks.</p>
<p>Which vector database you choose depends on scale, deployment constraints, and feature requirements. <strong>Pinecone</strong> and <strong>Weaviate</strong> offer managed services with minimal operational overhead. <strong>Milvus</strong>, <strong>Qdrant</strong>, and <strong>Chroma</strong> provide self-hosted options for data sovereignty requirements. <strong>pgvector</strong> extends PostgreSQL for teams that want to consolidate on existing infrastructure.</p>
<p>When evaluating options, look at: supported index types (HNSW, IVF, flat), filtering capabilities (metadata predicates during search), update semantics (real-time vs. batch), multi-tenancy support, and hybrid search features. For production systems, also check backup/restore procedures, monitoring integrations, and horizontal scaling characteristics.</p>
<p>Design your indexing strategy around your query patterns. If most queries filter by date range or document type, make sure those fields are indexed for efficient predicate pushdown. For <strong>large corpora</strong>, partition by logical boundaries (tenant, document category, time period) to limit search scope and improve latency.</p>
<h2 id="heading-retrieval-and-context-engineering">Retrieval and Context Engineering</h2>
<h3 id="heading-query-embedding-and-hybrid-recall">Query Embedding and Hybrid Recall</h3>
<p>Convert queries to <strong>embeddings</strong> and combine semantic similarity with keyword/boolean search to capture both intent and exact matches.</p>
<p>Pure <strong>semantic search</strong> is great at understanding intent but can miss exact terminology matches that matter in technical domains. A query about "401(k) contribution limits" needs to match documents containing that exact term, even if semantically similar phrases like "retirement savings caps" score higher. Hybrid search gives you both.</p>
<p>Implement hybrid recall using reciprocal rank fusion (RRF) or learned combination weights. RRF provides a parameter-free baseline: for each retrieval method, assign scores based on rank position (score = 1 / (k + rank)), then sum scores across methods. Learned weights require evaluation data but can significantly outperform simple fusion by calibrating relative reliability of each retrieval signal.</p>
<p>Query preprocessing improves both semantic and keyword recall. Expand acronyms, resolve pronouns using conversation context, and decompose complex queries into sub-queries for <strong>multi-hop reasoning</strong>. For conversational agents, maintain <strong>query context across turns</strong>: "tell me more about that" needs resolution against the previous retrieval context, not a fresh search.</p>
<h3 id="heading-top-k-selection-and-re-ranking">Top-K Selection and Re-ranking</h3>
<p>Retrieve top-K chunks (typical K = 3–5), then re-rank by relevance signals (semantic score; keyword overlap; recency; source trust).</p>
<p>Initial retrieval optimizes for recall; you're casting a wide net to make sure relevant chunks don't slip through. Re-ranking optimizes for precision, surfacing the best chunks from that initial set. This two-stage approach lets you use lightweight embeddings for broad recall, then apply expensive cross-encoder models for precise ranking.</p>
<p>Cross-encoder re-rankers (<strong>Cohere Rerank</strong>, <strong>BGE-reranker</strong>, <strong>MS MARCO fine-tuned models</strong>) jointly encode query and document, enabling richer relevance modeling than dot-product similarity. The latency cost is real though: cross-encoders process each query-document pair individually; so apply re-ranking only to the initial retrieval set, never the full corpus.</p>
<p>Beyond semantic relevance, incorporate domain-specific ranking signals: document recency (prefer current policies over outdated versions), source authority (official documentation over forum posts), user access permissions (filter results the user can't actually see), and historical click-through data where you have it.</p>
<h3 id="heading-chunk-combination-and-coherence">Chunk Combination and Coherence</h3>
<p>Merge related chunks into a <strong>single coherent context block</strong>; preserve ordering and add minimal connective prompts to avoid contradictory snippets.</p>
<p>Retrieved chunks often overlap, repeat information, or cover the same concept at different granularities. Naive concatenation wastes context tokens and can confuse the LLM with redundant or subtly inconsistent phrasings. Implement deduplication based on content similarity: if two chunks exceed a similarity threshold (say, 0.9 cosine similarity), keep only the more comprehensive version.</p>
<p>For chunks from the same document, restore original ordering and add minimal structural markers ("From section 3.2:", "Continuing from the previous passage:"). For chunks from different sources discussing the same topic, label source transitions explicitly and note any apparent conflicts rather than hiding them.</p>
<p>A technique worth trying: <em>context stuffing</em> that expands high-confidence chunks with surrounding content. If a 512 token chunk scores highly, fetching the preceding and following chunks often provides valuable context that improves answer quality. You're effectively implementing dynamic chunk sizing based on retrieval confidence.</p>
<h3 id="heading-compression-and-prioritization">Compression and Prioritization</h3>
<p>Summarize or compress low-value chunks and prioritize high-precision evidence to control <strong>token usage and latency</strong>.</p>
<p>Context window management is a bigger optimization surface than most people realize. Even with 100K+ token context windows, more content isn't always better. LLMs exhibit "lost in the middle" effects where information buried in long contexts gets less attention. Prioritize placement: put highest-relevance chunks at the beginning and end of the context block, with supporting evidence in the middle.</p>
<p>Tiered compression helps here: high-confidence chunks appear verbatim, medium-confidence chunks get summarized to key points, low-confidence chunks become one-line references with source links. LLM-based summarization can compress 4-5x while preserving key facts, though you're trading latency and risking some information loss.</p>
<p>For latency-sensitive applications, consider <strong>progressive retrieval</strong>: return an initial response based on top-1 or top-2 chunks while asynchronously retrieving and processing additional context for follow-up. Users generally prefer fast approximate answers with the option to <em>dig deeper</em> over consistently slow comprehensive responses.</p>
<h3 id="heading-provenance-and-evidence-surfacing">Provenance and Evidence Surfacing</h3>
<p>Attach <strong>chunk metadata and short citations</strong> to LLM outputs so agents can justify decisions and enable human verification.</p>
<p>Provenance isn't optional for enterprise RAG. Every factual claim in generated outputs should trace to specific source chunks, letting human reviewers verify accuracy and spot hallucinations. Implement citation generation as a core prompt engineering pattern: instruct the LLM to bracket claims with source references and explicitly flag statements not grounded in retrieved evidence.</p>
<p>Design citation formats for your use case. Inline references work for conversational responses; footnotes suit long-form documents. Include enough metadata for users to find the original source: document name, section heading, page number, and ideally a direct link or preview. For high-stakes domains (legal, medical, financial), consider citation verification that confirms generated citations actually appear in the referenced source.</p>
<p>Surfacing retrieval confidence alongside citations adds useful nuance: "According to the 2024 Employee Handbook (high confidence)..." versus "Based on a 2019 policy document that may be outdated (verify current policy)...". This <strong>calibrated uncertainty</strong> helps users weigh AI-generated information appropriately.</p>
<h2 id="heading-scaling-tradeoffs-and-operational-concerns">Scaling Tradeoffs and Operational Concerns</h2>
<h3 id="heading-diminishing-returns">Diminishing Returns</h3>
<p>More retrieved tokens can yield marginal gains and eventually degrade performance due to noise and LLM context limits; tune K and chunk size empirically.</p>
<p>The retrieval-quality curve is typically logarithmic. The first few high-quality chunks provide most of the value; each additional chunk contributes less. Past an inflection point, additional retrieval actually degrades output quality. Irrelevant chunks distract the model, near-duplicate content creates confusion, and sheer volume triggers "lost in the middle" attention issues.</p>
<p>Tune retrieval parameters empirically using held-out evaluation sets with human-judged relevance labels. Measure both retrieval metrics (<strong>precision@K, recall@K, MRR</strong>) and end-to-end answer quality (factual accuracy, completeness, hallucination rate). The optimal K varies by query type: simple factual lookups may need K=1-2, while complex analytical questions benefit from K=5-10.</p>
<p>Adaptive retrieval that adjusts K based on query characteristics or retrieval confidence is worth exploring. If the top-1 chunk scores way higher than alternatives (large margin), additional chunks probably add noise. If scores are clustered, more chunks may provide complementary perspectives worth including.</p>
<h3 id="heading-latency-and-cost">Latency and Cost</h3>
<p>Larger contexts increase inference time and billing; mitigate via <strong>chunk prioritization, compression, and local model hosting</strong>.</p>
<p>RAG systems stack multiple latency sources: embedding generation (10-50ms), vector search (10-100ms depending on scale and index type), chunk fetching (variable), optional re-ranking (100-500ms for cross-encoder models), and LLM inference (scales roughly linearly with context + output tokens). Profile each component to find your actual bottlenecks.</p>
<p>Cost optimization strategies include: <strong>caching embeddings</strong> for common queries, tiered storage (hot chunks in memory, cold in disk-backed stores), batching embedding requests, and using smaller models for initial retrieval with larger models only for final generation. For high-volume systems, embedding and re-ranking costs can actually exceed LLM inference costs; hence factor this into architecture decisions.</p>
<p>Implement circuit breakers and graceful degradation. If retrieval latency exceeds thresholds, fall back to reduced K, skip re-ranking, or serve from cache. Users generally prefer fast approximate answers with the option to dig deeper over consistently slow comprehensive responses.</p>
<h3 id="heading-data-curation-overhead">Data Curation Overhead</h3>
<p>High-quality ingestion (<strong>OCR fixes, table extraction, metadata capture</strong>) reduces downstream noise but increases upfront engineering effort.</p>
<p>"Garbage in, garbage out" applies forcefully to RAG. Poor source processing creates systematic retrieval failures that are hard to debug and impossible to fix without re-ingestion. Invest in ingestion quality proportional to the corpus's importance and expected query volume.</p>
<p>Set up continuous quality monitoring: track retrieval success rates by source document, identify chunks that frequently appear in retrievals but rarely in final answers (suggesting low utility), flag sources with high hallucination rates in downstream outputs. Use this telemetry to prioritize re-processing of problematic sources.</p>
<p>Build feedback loops from production usage. When users mark answers as incorrect or unhelpful, trace back to retrieved chunks and source documents. This creates a prioritized queue for manual review and re-curation, focusing human effort where it actually moves the needle on system quality.</p>
<h2 id="heading-local-models-and-runtime-optimizations">Local Models and Runtime Optimizations</h2>
<h3 id="heading-on-prem-hosting">On-prem Hosting</h3>
<p>Open-source runtimes (e.g.: <strong>vLLM, Llama C++</strong>) can run models locally for data sovereignty and lower per-call costs.</p>
<p>Local hosting starts making economic sense at scale. Once inference volume exceeds roughly $10K-20K/month in API costs, <strong>dedicated GPU infrastructure</strong> often provides better unit economics. The exact crossover depends on utilization rates, hardware costs, and operational overhead: model this carefully before committing.</p>
<p>Data sovereignty requirements often mandate local hosting regardless of economics. Regulated industries (healthcare, finance, government) may prohibit sending data to third-party APIs, and even non-regulated organizations increasingly prefer keeping <strong>sensitive data on-premises</strong>. Local hosting also eliminates external dependencies for <strong>availability-critical applications</strong>.</p>
<p>Model selection involves different tradeoffs for local hosting versus API usage. Smaller models (7B-13B parameters) run on consumer GPUs and provide acceptable quality for many tasks. Larger models (70B+) require multi-GPU setups but approach frontier model quality. Quantized versions (4-bit, 8-bit) reduce memory requirements 2-4x with modest quality degradation, letting you run larger models on smaller hardware.</p>
<h3 id="heading-runtime-tuning">Runtime Tuning</h3>
<p>Optimize KV cache, batching, and model runtime parameters to <strong>accelerate RAG and multi-agent throughput.</strong></p>
<p>KV cache management is the primary lever for inference optimization. The attention mechanism's key-value cache grows linearly with sequence length and eats substantial GPU memory. PagedAttention (vLLM's approach) manages cache memory dynamically, eliminating fragmentation and enabling higher batch sizes. For RAG workloads with predictable context patterns, pre-computing and caching KV states for common context prefixes can help significantly.</p>
<p>Continuous batching dramatically improves throughput by adding new requests to running batches as previous requests complete. Unlike static batching (waiting for a full batch before processing), <strong>continuous batching</strong> maintains high GPU utilization even with variable request arrival rates. <strong>vLLM</strong>, <strong>TensorRT-LLM</strong>, and other modern runtimes implement this by default.</p>
<p>Other tuning parameters worth exploring: tensor parallelism configuration for multi-GPU setups, speculative decoding (using a smaller model to draft tokens verified by the larger model), and flash attention implementations that reduce memory bandwidth requirements. Profile systematically; optimal configurations vary significantly across models, hardware, and workload characteristics.</p>
<h3 id="heading-api-compatibility">API compatibility</h3>
<p>Maintain the same API surface as cloud models where possible to <strong>simplify integration</strong> while benefiting from <strong>local performance</strong>.</p>
<p>Standardizing on OpenAI-compatible API formats lets you switch between providers seamlessly and simplifies testing. <strong>vLLM</strong>, <strong>Ollama</strong>, <strong>LocalAI</strong>, and most inference servers support OpenAI-compatible endpoints. This compatibility layer means you can develop against cloud APIs during prototyping, then deploy to local infrastructure without touching application code.</p>
<p>Abstract provider-specific features behind consistent interfaces: model names, embedding dimensions, token limits, and capability flags should all be configurable without code changes. Implement <strong>health checks, retry logic, and fallback chains</strong> that can route traffic between local and cloud providers based on availability and load.</p>
<p>Maintain parity testing that validates local deployments produce comparable outputs to reference cloud models. Quantization and different inference implementations can introduce subtle behavioral differences; automated regression testing catches these before they hit production quality.</p>
<h2 id="heading-agentic-patterns-and-rag-integration">Agentic Patterns and RAG Integration</h2>
<h3 id="heading-agent-roles">Agent Roles</h3>
<p>Typical multi-agent patterns: <strong>planner/architect → implementer → reviewer</strong> for coding; triage and specialized agents for support/HR workflows.</p>
<p>Dividing cognitive labor across specialized agents mirrors how effective human teams work. Planner agents excel at decomposing complex tasks, managing dependencies, and maintaining coherent high-level strategy. Implementer agents focus on executing specific subtasks with deep domain expertise. Reviewer agents apply quality control, catch errors, and ensure outputs meet requirements.</p>
<p>For coding workflows, a common pattern includes an architect agent that designs system structure and interfaces, implementer agents specialized by technology (frontend, backend, database), and a reviewer agent checking for bugs, security issues, and style violations. <strong>Each agent can have tailored RAG access</strong>; the architect queries design pattern documentation while implementers access language-specific references.</p>
<p>Support workflows benefit from triage agents that classify incoming requests, route to specialized agents (billing, technical, account management), and escalate to humans when confidence is low. Each specialized agent maintains its own knowledge base and RAG configuration optimized for its domain. Implement clear handoff protocols: when an agent transfers responsibility, it should pass relevant context and retrieval results rather than forcing the receiving agent to rediscover information.</p>
<h3 id="heading-tool-calling-and-protocols">Tool Calling and Protocols</h3>
<p>Use standardized protocols (e.g., model context protocols) for reliable <strong>service/API calls</strong> and agent coordination.</p>
<p>Robust tool calling requires careful schema design. Define clear input/output contracts, handle partial failures gracefully, and implement timeout and retry policies appropriate to each tool's characteristics. Tools should be idempotent where possible: agents may retry operations due to transient failures, and non-idempotent tools risk duplicating side effects.</p>
<p><strong>Model Context Protocol</strong> (MCP) and similar standards provide structured frameworks for tool definition, capability discovery, and result formatting. Standardized protocols enable tool reuse across agents and simplify debugging through consistent logging and tracing formats. Tool registries that agents can query to discover available capabilities dynamically are worth building.</p>
<p>Agent coordination patterns include direct messaging (<strong>agents communicate point-to-point</strong>), blackboard systems (agents read/write to shared state), and orchestrator patterns (a central coordinator assigns tasks and collects results). Choose based on workflow complexity: simple linear pipelines work fine with direct messaging, while complex workflows with conditional branching and parallel execution benefit from explicit orchestration.</p>
<h3 id="heading-observation-and-feedback-loops">Observation and Feedback Loops</h3>
<p>Instrument agents to observe outcomes, log evidence, and update memory or retraining signals to reduce repeated hallucinations.</p>
<p><strong>Comprehensive observability</strong> is essential for debugging and improving agentic systems. Log every agent action: tool calls with inputs/outputs, retrieval queries with results, reasoning traces, final outputs. Structure logs for analysis so you can run queries like "show all cases where the billing agent called the refund tool after retrieval returned no results."</p>
<p>Implement outcome tracking that connects agent actions to downstream results. Did the generated code compile? Did the customer issue get resolved? Did the user accept or reject the suggestion? This feedback enables identifying failure patterns and measuring improvement over time.</p>
<p>Build learning loops that translate observations into system improvements. Common failure patterns should trigger knowledge base updates, retrieval tuning, or prompt refinements. For high-volume systems, automated anomaly detection that flags unusual patterns (sudden spike in tool failures, retrieval returning empty results, agent loops) for human investigation pays dividends.</p>
<h3 id="heading-rag-as-a-guardrail">RAG as a Guardrail</h3>
<p>Integrate RAG retrieval into <strong>agent decision paths</strong> so agents <strong>consult evidence before acting</strong> and attach retrieved passages for traceability.</p>
<p>RAG transforms agents from confident confabulators into evidence-grounded reasoners. Design agent prompts to require evidence: "Before recommending an action, <strong>retrieve relevant documentation and cite specific passages</strong> supporting your recommendation." This forces deliberate consultation rather than relying solely on parametric knowledge.</p>
<p>Implement retrieval triggers at decision points: before executing irreversible actions, before providing factual claims to users, before contradicting previous agent outputs. Make absence of evidence explicit in prompts: "If no relevant documentation is found, state that the recommendation is based on general knowledge rather than verified sources."</p>
<p>Use <strong>retrieval confidence</strong> as an escalation signal. Low-confidence retrievals for high-stakes decisions should trigger human review rather than autonomous action. Combine retrieval-based grounding with output validation, and check generated outputs against retrieved sources to catch hallucinations that slipped through generation.</p>
]]></content:encoded></item><item><title><![CDATA[Results and errors handling strategies (in C#)]]></title><description><![CDATA[Overview
A consistent and robust strategy to propagate results and errors is key for both code correctness and solidity.
Talking about results propagation we cannot avoid bring up functional programming. Behavior of purely functional code is predicta...]]></description><link>https://jackcoder.hashnode.dev/functional-result-strategies-csharp</link><guid isPermaLink="true">https://jackcoder.hashnode.dev/functional-result-strategies-csharp</guid><category><![CDATA[Functional Programming]]></category><category><![CDATA[error handling]]></category><category><![CDATA[C#]]></category><dc:creator><![CDATA[Giacomo Stelluti Scala]]></dc:creator><pubDate>Sun, 11 Feb 2024 19:10:53 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1705233019891/01001e9d-3c8e-4e4a-a356-300c69c1f754.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="heading-overview">Overview</h3>
<p>A consistent and robust strategy to propagate results and errors is key for both code <strong>correctness</strong> and <strong>solidity</strong>.</p>
<p>Talking about results propagation we cannot avoid bring up <strong>functional programming</strong>. Behavior of purely functional code is predictable cause it's comparable to a <strong>mathematical expression</strong>.</p>
<p>Anyway a completely functional approach is in some cases unpractical. This explains the success of <strong>multi-paradigm languages</strong> like C#. We can also state how designers included more functional constructs to its syntax in each new release of the compiler.</p>
<p>Functional languages provide result types to propagate result values among functions. The key point is that even in a multi-paradigm language we can write (pure or almost) functional code to get rid of the use of null and exceptions. This is achieved using result types.</p>
<h3 id="heading-discussing-result-types">Discussing result types</h3>
<p>For example in the .NET world F# has the <strong>Option'T</strong> type that can wrap a value as <strong>Some'T</strong> or be in form the of <strong>None</strong> when empty. Types of this kind are defined <a target="_blank" href="https://en.wikipedia.org/wiki/Monad_(functional_programming)">monads</a> and in our case are used to avoid the use of <strong>null</strong> to represents the absence of a scalar value. F# as being part of .NET family still has syntax for null, while other more strictly functional languages like Haskell simply lack it at all.</p>
<p>For completeness the Haskell equivalent of Option'T is <strong>Maybe</strong>, defined as: <code>Maybe a = Nothing | Just a</code></p>
<p>Sequences are represented in .NET by <strong>IEnumerable&lt;T&gt;</strong> can be viewed as monads, since a sequence has two forms: an empty sequence and a sequence holding values.</p>
<p>.NET BCL lacks a type like F# <strong>Option'T</strong>, the most closest is <strong>Nullable&lt;T&gt;</strong> used to wrap a value type into a nullable reference type (but as you can see with a different purpose). Back to Option-like types but you can find some implementations in NuGet. For simplicity in next samples I will use result types from a library that I personally designed: <a target="_blank" href="https://github.com/gsscoder/sharpx">SharpX</a>.</p>
<p>A C# method returning Maybe&lt;T&gt; looks like that:</p>
<pre><code class="lang-csharp"><span class="hljs-function">Maybe&lt;<span class="hljs-keyword">ushort</span>&gt; <span class="hljs-title">ComputeKcals</span>(<span class="hljs-params">Food[] foods</span>)</span> =&gt; foods.Any()
  ? Maybe.Just(foods.Sum(x =&gt; x.Kcal))
  : Maybe.Nothing&lt;<span class="hljs-keyword">ushort</span>&gt;();
</code></pre>
<p>And here follows the code that consumes a the Maybe&lt;T&gt; value:</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">if</span> (ComputeKcals(foods).MatchJust(<span class="hljs-keyword">out</span> <span class="hljs-keyword">var</span> kcals)) {
  Console.WriteLine(<span class="hljs-string">$"Total calories: <span class="hljs-subst">{kcals}</span>"</span>);
} <span class="hljs-keyword">else</span> {
  Console.WriteLine(<span class="hljs-string">"Failed to compute calories: food list is empty"</span>);
}
</code></pre>
<p>It's needed to quote another useful result type borrowed from Haskell: <strong>Either&lt;TLeft, TRight&gt;</strong>. This type can be in form of <strong>Left&lt;TLeft&gt;</strong> in case of failure and in form of <strong>Right&lt;TRight&gt;</strong> in case of success (easy to remind).</p>
<ul>
<li><p><strong>TLeft</strong> can be anything like a string with an error message, an Exception or a custom data structure designed to hold failure details (let's explore this later).</p>
</li>
<li><p><strong>TRight</strong> will be any value produced by the computation.</p>
</li>
</ul>
<p>Let's refactor the previous sample using Either:</p>
<pre><code class="lang-csharp"><span class="hljs-function">Either&lt;<span class="hljs-keyword">string</span>, <span class="hljs-keyword">ushort</span>&gt; <span class="hljs-title">ComputeKcals</span>(<span class="hljs-params">Food[] foods</span>)</span> =&gt; foods.Any()
  ? Either.Right(foods.Sum(x =&gt; x.Kcal))
  : Either.Left(<span class="hljs-string">"Failed to compute calories: food list is empty"</span>);

<span class="hljs-keyword">var</span> result = ComputeKcals(foods);
<span class="hljs-keyword">if</span> (result.MatchRight(<span class="hljs-keyword">out</span> <span class="hljs-keyword">var</span> kcals)) {
  Console.WriteLine(<span class="hljs-string">$"Total calories: <span class="hljs-subst">{kcals}</span>"</span>);
} <span class="hljs-keyword">else</span> {
  Console.WriteLine(result.FromLeft());
}
</code></pre>
<p>One thing that is necessary to point out now is that the use these types can dramatically reduce the need to design new exceptions in your application. As a rule of thumb <strong>as long as an error can be treated as a value you don't need to throw an exception</strong> (just return it as a result).</p>
<p>Exceptions as the name itself suggests should be reserved for exceptional cases, for the <strong>unexpected</strong>.</p>
<h3 id="heading-use-of-maybe-and-either">Use of Maybe and Either</h3>
<p>You can think of <strong>Maybe</strong> type as a mean to avoid handling null values and <strong>Either</strong> as a Maybe that can hold errors data in case of a failure. In my implementation (and hopefully in many others) both types are <strong>defined as struct</strong> so they cannot be null.</p>
<p>I used these types from years in various open and closed source projects, developing patterns to fit specific needs.</p>
<p>For example when a method returns a <strong>boolean</strong> value and you want also to supply error details, you can still use Maybe. Let's say we've a method like the following one:</p>
<pre><code class="lang-csharp"><span class="hljs-function"><span class="hljs-keyword">bool</span> <span class="hljs-title">DeployFunctionApp</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> resourceName</span>)</span>
{
  <span class="hljs-keyword">try</span> {
    _ = _armClient.DeployResource(resourceName, Resources.FunctionAppBicep);
  } <span class="hljs-keyword">catch</span> (ArmException ex) {
    _logger.LogCritical(ex, <span class="hljs-string">$"Failed <span class="hljs-subst">{resourceName}</span> deployment"</span>);
  }
}
</code></pre>
<p>This is just a variant defined in using a non-functional design. The author of the code could have relaunched the exception (inside a custom one or less), relying in a <strong>global handler</strong>. There are many ways to refactor this code in a functional way. It depends on the eventual need for logging and/or generating a more synthetic (or human friendly) error message.</p>
<p>We could refactor it using wrapping a tuple inside a Maybe type:</p>
<pre><code class="lang-csharp">Maybe&lt;(<span class="hljs-keyword">string</span>, Exception)&gt; DeployFunctionApp(<span class="hljs-keyword">string</span> resourceName)
{
  <span class="hljs-keyword">try</span> {
    _ = _armClient.DeployResource(resourceName, Resources.FunctionAppBicep);
  } <span class="hljs-keyword">catch</span> (ArmException ex) {
    <span class="hljs-keyword">return</span> Maybe.Just((<span class="hljs-string">$"Failed <span class="hljs-subst">{resourceName}</span> deployment"</span>,
      <span class="hljs-keyword">new</span> DeployException(resourceName, Kind.FunctionApp, ex)));
  }
  <span class="hljs-keyword">return</span> Maybe.Nothing&lt;(<span class="hljs-keyword">string</span>, Exception)&gt;();
}
</code></pre>
<p>In this case a Maybe in form of Nothing represents a success: no errors. The consumer of this method could log the exception or relaunch it wrapped in an another one. If have to I would suggest to launch exceptions only in the topmost part of the code: be the Program class or an ASPNET Core controller.</p>
<ul>
<li><p>In a Console application you can centralize the exception handling defining the <a target="_blank" href="https://learn.microsoft.com/en-us/dotnet/api/system.appdomain.unhandledexception?view=net-8.0">UnhandledException</a> event for the current application domain.</p>
</li>
<li><p>In an ASPNET Core application you can write a specific <a target="_blank" href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling?view=aspnetcore-8.0#iexceptionhandler">middleware</a> to handle exceptions in a central point.</p>
</li>
</ul>
<p>Passing instanced exceptions as results and throwing them in their original state <em>will fake</em> the <strong>stack trace</strong>. In that case I would recommend to throw a new specific exception that wraps the original one. Anyway when you treating exception as mere data structures that hold failure data the stack trace lose its relevance, since you're relying predictability of your program flow.</p>
<p>To improve the previous method and consolidate a pattern through all the codebase, we can consider defining a type instead of using a tuple:</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">readonly</span> <span class="hljs-keyword">struct</span> Failure {
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> Message { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">init</span>; }
    <span class="hljs-keyword">public</span> Exception Exception { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">init</span>; }
}
</code></pre>
<p>The refactored method will be as follows:</p>
<pre><code class="lang-csharp"><span class="hljs-function">Maybe&lt;Failure&gt; <span class="hljs-title">DeployFunctionApp</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> resourceName</span>)</span>
{
  <span class="hljs-keyword">try</span> {
    _ = _armClient.DeployResource(resourceName, Resources.FunctionAppBicep);
  } <span class="hljs-keyword">catch</span> (ArmException ex) {
    <span class="hljs-keyword">return</span> Maybe.Just(<span class="hljs-keyword">new</span> Failure {
      Message = <span class="hljs-string">$"Failed <span class="hljs-subst">{resourceName}</span> deployment"</span>,
      Exception = <span class="hljs-keyword">new</span> DeployException(resourceName, Kind.FunctionApp, ex));
  }
  <span class="hljs-keyword">return</span> Maybe.Nothing&lt;Failure&gt;();
}
</code></pre>
<p>Let say that we need to return a resource identifier from our DeployFunctionApp method. For this eventuality we can rewrite it using Either as result type:</p>
<pre><code class="lang-csharp"><span class="hljs-function">Either&lt;Failure, <span class="hljs-keyword">string</span>&gt; <span class="hljs-title">DeployFunctionApp</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> resourceName</span>)</span>
{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">var</span> resource = _armClient.DeployResource(resourceName, Resources.FunctionAppBicep);
    <span class="hljs-keyword">return</span> Either.Right&lt;Failure, <span class="hljs-keyword">string</span>&gt;(resource.Id);
  } <span class="hljs-keyword">catch</span> (ArmException ex) {
    <span class="hljs-keyword">return</span> Either.Left&lt;Failure, <span class="hljs-keyword">string</span>&gt;(<span class="hljs-keyword">new</span> Failure {
      Message = <span class="hljs-string">$"Failed <span class="hljs-subst">{resourceName}</span> deployment"</span>,
      Exception = <span class="hljs-keyword">new</span> DeployException(resourceName, Kind.FunctionApp, ex));
  }
}
</code></pre>
<p>In synthesis the advantage of using properly designed result types are the following.</p>
<ul>
<li><p><strong>Predictability</strong>. A program with a flow based on results are more predictable:</p>
<ul>
<li><p>it's easier to reason about the expected behavior of a method/function</p>
</li>
<li><p>the resulting code is more resilient to errors as developers are compelled to consider and handle all possible outcomes.</p>
</li>
</ul>
</li>
<li><p><strong>Robustness</strong>. Results are implemented as value types:</p>
<ul>
<li>the validation at compile time will completely avoid any <strong>NullReferenceException</strong> at runtime.</li>
</ul>
</li>
<li><p><strong>Immutability</strong>. Result types promotes immutability:</p>
<ul>
<li><p>accidental modifications are prevented</p>
</li>
<li><p>a consistent representation of success or failure is ensured throughout the program.</p>
</li>
</ul>
</li>
</ul>
<h3 id="heading-conclusion">Conclusion</h3>
<p>That being said, do we really need to avoid nulls and exceptions to the maximum extent? In first instance we technically completely can't and more we shouldn't, so the answer is <em>no</em>.</p>
<p>In C# as in other languages these are <strong>first class constructs</strong> deeply rooted into the their design. Being dogmatic and extremist is rarely a good idea and this case is not an exception (<em>pardon the redundancy</em>).</p>
<p>Anyway for reusable code or code that could be maintained by others, I strongly recommend that you to take a clear decision taking into account these suggestions:</p>
<ul>
<li><p>select which canonical result type you want to use</p>
</li>
<li><p>evaluate the need to design your own custom result types</p>
</li>
<li><p>decide to use or avoid result types in public signatures (e.g. <em>if you're designing an open source class library or one shared among a wide organization</em>)</p>
</li>
<li><p>decide to break these functional patterns for very internal parts of code (e.g. <em>for performance reasons or for less verbose code</em>).</p>
</li>
</ul>
<p>Functional is funky! (<em>but not this joke</em>)</p>
]]></content:encoded></item></channel></rss>