<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Charan Reddy Rachumalla</title>
    <description>The latest articles on DEV Community by Charan Reddy Rachumalla (@charanrachumalla).</description>
    <link>https://dev.to/charanrachumalla</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4198917%2Febde7395-fd17-4c7a-8851-193909658f20.png</url>
      <title>DEV Community: Charan Reddy Rachumalla</title>
      <link>https://dev.to/charanrachumalla</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/charanrachumalla"/>
    <language>en</language>
    <item>
      <title>Every Step Runs Inside poll(): The Kafka Consumer Flow, End to End</title>
      <dc:creator>Charan Reddy Rachumalla</dc:creator>
      <pubDate>Wed, 21 Oct 2026 17:43:33 +0000</pubDate>
      <link>https://dev.to/charanrachumalla/every-step-runs-inside-poll-the-kafka-consumer-flow-end-to-end-2kin</link>
      <guid>https://dev.to/charanrachumalla/every-step-runs-inside-poll-the-kafka-consumer-flow-end-to-end-2kin</guid>
      <description>&lt;p&gt;Every Kafka consumer application calls &lt;code&gt;poll()&lt;/code&gt;. Very few developers can list what happens inside it.&lt;/p&gt;

&lt;p&gt;That gap shows up at the worst time: a consumer that hangs, a group that rebalances in a loop, a commit that keeps failing. Each of those failures lives in a specific step of the flow below. The flow is seven steps. The first is setup, the next four are a handshake, and the last two are the steady state the consumer spends its life in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Create consumer and subscribe
&lt;/h2&gt;

&lt;p&gt;Application code builds a &lt;code&gt;KafkaConsumer&lt;/code&gt; with &lt;code&gt;bootstrap.servers&lt;/code&gt; and a &lt;code&gt;group.id&lt;/code&gt;, then calls &lt;code&gt;subscribe()&lt;/code&gt; with the topic list.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;subscribe()&lt;/code&gt; only registers interest. Nothing is fetched, nothing is assigned, no broker knows the consumer exists yet. Every step that follows is driven by &lt;code&gt;poll()&lt;/code&gt; — it is the engine of the entire client, and the handshake below unfolds across its first calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: FindCoordinator
&lt;/h2&gt;

&lt;p&gt;The consumer asks any bootstrap broker a simple question: which broker coordinates this group.&lt;/p&gt;

&lt;p&gt;The answer is the broker that leads the &lt;code&gt;__consumer_offsets&lt;/code&gt; partition assigned to the group. In this walkthrough that is Broker 3, coordinating the group &lt;code&gt;payments-group&lt;/code&gt;. From this point, all group traffic — joins, heartbeats, offset commits — goes to Broker 3. None of it goes to the brokers holding the actual data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: JoinGroup
&lt;/h2&gt;

&lt;p&gt;The consumer sends a JoinGroup request carrying its protocol preferences and timeouts:&lt;/p&gt;

&lt;p&gt;group.id=payments-group&lt;br&gt;
session.timeout.ms=45000&lt;br&gt;
rebalance.timeout.ms=300000&lt;/p&gt;

&lt;h1&gt;
  
  
  assignment protocols offered: range, cooperative-sticky
&lt;/h1&gt;

&lt;p&gt;The coordinator starts a rebalance and elects a group leader from the members. It issues a generation number — generation 17 in this walkthrough — plus a member ID for each consumer. The generation matters later: it is how the coordinator detects members acting on a stale view of the group.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: SyncGroup
&lt;/h2&gt;

&lt;p&gt;The elected leader computes the partition assignment for every member and sends it back through the coordinator. SyncGroup returns this consumer's share: partitions &lt;code&gt;payments-0&lt;/code&gt; and &lt;code&gt;payments-1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Only now does the consumer know what it owns. Everything before this step was negotiation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: OffsetFetch
&lt;/h2&gt;

&lt;p&gt;The consumer asks the coordinator — not the partition leaders — where the group left off. The committed offsets come back: &lt;code&gt;payments-0&lt;/code&gt; → 100, &lt;code&gt;payments-1&lt;/code&gt; → 250.&lt;/p&gt;

&lt;p&gt;Position starts from the committed offset. If a partition has no committed offset at all, &lt;code&gt;auto.offset.reset&lt;/code&gt; decides where to start. That one configuration quietly determines whether a brand-new group reads a topic from the beginning or only sees new records.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Fetch
&lt;/h2&gt;

&lt;p&gt;Fetch requests go to each partition's leader broker — this is the data path, separate from the coordinator path. &lt;code&gt;payments-0&lt;/code&gt; is fetched from Broker 1, &lt;code&gt;payments-1&lt;/code&gt; from Broker 2.&lt;/p&gt;

&lt;p&gt;Each fetch starts at the consumer's position, and record batches stream back per &lt;code&gt;poll()&lt;/code&gt;. Fetch is a long poll: the broker holds the request, waiting up to &lt;code&gt;fetch.max.wait.ms&lt;/code&gt; to accumulate at least &lt;code&gt;fetch.min.bytes&lt;/code&gt; of data. An idle topic does not mean a stuck consumer. It usually means the broker is patiently waiting to send a batch worth sending.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Process and commit
&lt;/h2&gt;

&lt;p&gt;Application code processes the records, then commits. The commit carries the &lt;em&gt;next&lt;/em&gt; offset: after processing through offset 109 on &lt;code&gt;payments-0&lt;/code&gt;, the committed offset is 110. The coordinator writes it into &lt;code&gt;__consumer_offsets&lt;/code&gt;, and that write is the group's durable progress.&lt;/p&gt;

&lt;p&gt;A commit can fail on a stale generation. That failure means a rebalance happened mid-processing — the correct response is to rejoin and retry, not to hammer the commit. After a successful commit, the flow loops back to Fetch. Steps 6 and 7 repeat for the life of the consumer. The handshake in steps 2 through 5 only reruns when a rebalance forces it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Under the hood
&lt;/h2&gt;

&lt;p&gt;The Java client splits this flow across a few key classes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;KafkaConsumer&lt;/strong&gt; — the API application code touches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ConsumerCoordinator&lt;/strong&gt; — group management: FindCoordinator, JoinGroup, SyncGroup, OffsetFetch, OffsetCommit, and heartbeats.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fetcher&lt;/strong&gt; — records from the partition leaders.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SubscriptionState&lt;/strong&gt; — the current assignment and position.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NetworkClient&lt;/strong&gt; — everything on the wire.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The split explains the two network paths. Group management runs through the coordinator, Broker 3. The data path runs to the leaders, Broker 1 and Broker 2. Committed progress at the end of this walkthrough: &lt;code&gt;payments-0&lt;/code&gt; → 110, &lt;code&gt;payments-1&lt;/code&gt; → 260, stored in &lt;code&gt;__consumer_offsets&lt;/code&gt; where the next generation of the group will find it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule of thumb
&lt;/h2&gt;

&lt;p&gt;When a consumer hangs, the useful move is naming the step that broke: the handshake (steps 2–5), the fetch (step 6), or the commit (step 7).&lt;/p&gt;

&lt;p&gt;It is almost never the &lt;code&gt;process()&lt;/code&gt; code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;subscribe()&lt;/code&gt; registers interest and nothing more. &lt;code&gt;poll()&lt;/code&gt; drives every step that follows.&lt;/li&gt;
&lt;li&gt;Group traffic and data traffic take different paths: coordination to one broker, fetches to the partition leaders.&lt;/li&gt;
&lt;li&gt;A rebalance is the handshake rerunning. Generation numbers are how stale members get caught.&lt;/li&gt;
&lt;li&gt;A commit stores the next offset to read, and committed offsets in &lt;code&gt;__consumer_offsets&lt;/code&gt; are the group's only durable memory.&lt;/li&gt;
&lt;li&gt;Consumer problems debug fastest by step: handshake, fetch, or commit.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kafka</category>
      <category>java</category>
      <category>distributedsystems</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Kafka MCP Server: How a Copilot Agent Safely Operates a Real Kafka Cluster</title>
      <dc:creator>Charan Reddy Rachumalla</dc:creator>
      <pubDate>Wed, 21 Oct 2026 17:36:20 +0000</pubDate>
      <link>https://dev.to/charanrachumalla/kafka-mcp-server-how-a-copilot-agent-safely-operates-a-real-kafka-cluster-58df</link>
      <guid>https://dev.to/charanrachumalla/kafka-mcp-server-how-a-copilot-agent-safely-operates-a-real-kafka-cluster-58df</guid>
      <description>&lt;p&gt;I built an MCP server for Kafka. It is a backend that connects to Kafka brokers and exposes Kafka operations as tools, defined through the Model Context Protocol, so a Copilot agent can call them. An operator asks a plain-English operational question — a consumer group falling behind, a topic that needs a closer look — and the agent answers from the live cluster instead of from memory.&lt;/p&gt;

&lt;p&gt;The interesting part was never the model. The interesting part was everything around it: the tools, the permissions, the rules. This is the design, layer by layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 1: You and the copilot
&lt;/h2&gt;

&lt;p&gt;The interaction starts in plain English. The operator states the problem in ordinary language. The agent interprets the intent behind the ask, then picks the right tool from the set it has been given.&lt;/p&gt;

&lt;p&gt;That last clause carries the whole design. The agent chooses from a fixed toolset. It does not get a shell, a Kafka CLI, or a blank connection string. Its world is exactly the tools on the server, nothing wider.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 2: The MCP server
&lt;/h2&gt;

&lt;p&gt;The server defines each operation as a tool with a typed schema. Inputs are precise: a topic name is a string with constraints, a group ID is a group ID. There is no free-form parameter for the agent to improvise into.&lt;/p&gt;

&lt;p&gt;Two tools show the pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Describe topic&lt;/strong&gt; returns partitions, configs, ISR, and offsets for a topic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consumer lag&lt;/strong&gt; returns group lag read from live metadata — committed offsets compared against the latest offsets, per partition.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each tool is a narrow, named operation against the cluster. The server, not the agent, owns the broker connection. That connection authenticates with SASL or OAuth and runs with least-privilege permissions: the credentials can do what the tools need and nothing beyond that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 3: The Kafka cluster
&lt;/h2&gt;

&lt;p&gt;On the other side sits the cluster itself — brokers, topics and partitions, consumer groups with their membership, offsets, and lag, and the health signals: under-replicated partitions, offline partitions, alerts.&lt;/p&gt;

&lt;p&gt;The cluster is the live source of truth. Metadata is fetched fresh on every tool call. The agent never answers from a cached snapshot or from patterns in its training data. A lag number in the response is a lag number the cluster reported a second ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design rule 1: Tools are the contract
&lt;/h2&gt;

&lt;p&gt;A vague schema produces improvised inputs, and improvised inputs against a production cluster are a liability. Typed tools keep the agent honest. The contract is written once, by a person, in the schema — and every call the agent makes is forced through it.&lt;/p&gt;

&lt;p&gt;This is the same lesson APIs taught a decade ago, applied to a new kind of caller. The caller is probabilistic. The contract must not be.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design rule 2: Live state, not guesses
&lt;/h2&gt;

&lt;p&gt;An LLM can produce a fluent explanation of consumer lag with no cluster access at all. It will sound right and it may be wrong. The fix is retrieval from the system itself: answers grounded in metadata the cluster just returned.&lt;/p&gt;

&lt;p&gt;Every operational claim the agent makes traces back to a tool result. Strip the tool results away and the agent has nothing to say about this cluster — which is exactly the correct behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design rule 3: Destructive means a human gate
&lt;/h2&gt;

&lt;p&gt;Read tools can run freely. Anything that changes the cluster stops for a person. Always.&lt;/p&gt;

&lt;p&gt;This rule is enforced in the server, not requested in a prompt. Prompts are suggestions. Server-side gating is a mechanism. An agent that cannot reach a destructive operation without human approval is an agent that can be left running next to production.&lt;/p&gt;

&lt;h2&gt;
  
  
  The RAG side
&lt;/h2&gt;

&lt;p&gt;Tools cover live state. They do not cover knowledge: runbooks, design documents, postmortems, the accumulated written memory of a platform team. That is the retrieval side of the system.&lt;/p&gt;

&lt;p&gt;Documents are embedded and stored in a vector store. At ask time, the relevant passages are retrieved and handed to the model — Claude, running on Amazon Bedrock — which answers grounded in the retrieved text. The agent ends up with both halves of an operator's brain: the current state of the cluster from tools, and the team's documented experience from retrieval.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inference where the data already flows
&lt;/h2&gt;

&lt;p&gt;One more piece fits naturally into this stack. Flink UDFs can run LLM inference in-stream, enriching events as they pass through the pipeline. Not every record needs a model call, and the stream is the right place to decide which ones do. Enrichment happens where the data already flows, instead of in a separate service the data has to be shipped to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An agent operating infrastructure should work through a fixed set of typed tools, never through open-ended access.&lt;/li&gt;
&lt;li&gt;Typed schemas are the contract, and the contract is what keeps a probabilistic caller predictable.&lt;/li&gt;
&lt;li&gt;Operational answers must come from live system state, retrieved at call time — never from model memory.&lt;/li&gt;
&lt;li&gt;Destructive operations belong behind a human gate enforced by the server, not by the prompt.&lt;/li&gt;
&lt;li&gt;Tools give an agent the present. Retrieval gives it the past. A useful operator needs both.&lt;/li&gt;
&lt;li&gt;The model is the easy part. The system around it is the job.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kafka</category>
      <category>ai</category>
      <category>mcp</category>
      <category>platformengineering</category>
    </item>
    <item>
      <title>Events In, Answers Out: One Pipeline From Kafka Stream to Iceberg Table</title>
      <dc:creator>Charan Reddy Rachumalla</dc:creator>
      <pubDate>Wed, 21 Oct 2026 17:35:38 +0000</pubDate>
      <link>https://dev.to/charanrachumalla/events-in-answers-out-one-pipeline-from-kafka-stream-to-iceberg-table-2lka</link>
      <guid>https://dev.to/charanrachumalla/events-in-answers-out-one-pipeline-from-kafka-stream-to-iceberg-table-2lka</guid>
      <description>&lt;p&gt;Most streaming diagrams stop at the stream processor. Data flows in, something processes it, and the diagram ends with an arrow pointing at a box labeled "analytics."&lt;/p&gt;

&lt;p&gt;That last box is where the real design work lives. A stream is not something an analyst can query. A table is. The pipeline below is the shape I keep coming back to: sources, Kafka, Flink, Iceberg via TableFlow, Athena, QuickSight. Events in, answers out.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline at a glance
&lt;/h2&gt;

&lt;p&gt;Six stages, each doing one job:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Sources&lt;/strong&gt; — applications and services emitting events&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kafka&lt;/strong&gt; — the topics that hold those events&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apache Flink&lt;/strong&gt; — real-time processing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Iceberg tables via TableFlow&lt;/strong&gt; — stored on S3, cataloged in Glue&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Athena&lt;/strong&gt; — plain SQL queries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QuickSight&lt;/strong&gt; — dashboards&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No stage reaches around another. Each one consumes the output of the previous stage and produces the input for the next. That discipline is most of the design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stages 1 and 2: Sources and Kafka
&lt;/h2&gt;

&lt;p&gt;Everything starts with applications and services emitting events — an order placed, a payment attempted, a reading reported. Those events land on Kafka topics.&lt;/p&gt;

&lt;p&gt;Kafka earns its place here for two reasons. It decouples producers from everything downstream, so a source system never waits on analytics and never knows analytics exists. And it retains the events, so the processing layer can re-read history when logic changes or a job restarts from a failure.&lt;/p&gt;

&lt;p&gt;The topic becomes the contract: a named, ordered, replayable record of what happened. Everything downstream trusts the topic instead of trusting the source systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 3: Flink does the work in real time
&lt;/h2&gt;

&lt;p&gt;Flink reads from the topics and processes events as they arrive. This is where the raw stream gets shaped: noise filtered out, streams joined, events enriched, values aggregated over time windows.&lt;/p&gt;

&lt;p&gt;The important property is timing. The work happens once, at event time, while the data is fresh. The alternative is a batch job that re-reads an entire day of raw events every night and redoes the same shaping work from scratch. By the time records leave Flink, they are already in the shape the table needs.&lt;/p&gt;

&lt;p&gt;State lives in Flink for exactly this reason. A join or a window does not care that the data arrived as a stream. It cares that the right records meet each other, and Flink holds them until they do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 4: TableFlow lands the stream in Iceberg
&lt;/h2&gt;

&lt;p&gt;This is the stage that changed the architecture for me. TableFlow materializes the processed stream as an Apache Iceberg table, continuously. The data files live in Amazon S3. The table is registered in the Glue Catalog, and that catalog entry is what makes it visible to the rest of the analytics stack.&lt;/p&gt;

&lt;p&gt;Iceberg matters because it gives files on object storage real table semantics: a defined schema that can evolve, snapshots of the table at points in time, and safe reads while new data keeps arriving. The stream stops being a stream at exactly this point. It becomes a table, and it stays current because the materialization never stops.&lt;/p&gt;

&lt;p&gt;The practical win is what disappears. There is no separate ETL job copying Kafka data into a warehouse on a schedule. There is no second version of the pipeline maintained for the analytics side, drifting out of sync with the streaming version. The streaming path and the table are the same path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stages 5 and 6: Athena and QuickSight
&lt;/h2&gt;

&lt;p&gt;With the table in the Glue Catalog, Athena queries it with plain SQL. There is no cluster to size and no data to load. The query runs where the data already sits, on S3.&lt;/p&gt;

&lt;p&gt;QuickSight sits on top of that for dashboards. The people consuming the data never touch Kafka or Flink. They open a dashboard, or run a SQL query against a table that was a stream moments earlier.&lt;/p&gt;

&lt;p&gt;That separation is the point of the whole design. The streaming stack does streaming work. The analytics stack does analytics work. Iceberg and TableFlow are the seam between them, and the seam is a table format and a catalog — boring, standard, inspectable pieces.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A streaming pipeline is only finished when the data lands somewhere queryable. The last box on the diagram deserves the most design attention.&lt;/li&gt;
&lt;li&gt;Kafka works best as the durable contract between producers and processing: ordered, replayable, and decoupled from both sides.&lt;/li&gt;
&lt;li&gt;Processing in Flink at event time removes the nightly batch re-processing step entirely.&lt;/li&gt;
&lt;li&gt;Materializing a stream into Iceberg with TableFlow makes one pipeline serve as both the streaming path and the analytics path.&lt;/li&gt;
&lt;li&gt;Analysts should query tables with SQL. They should never need to know a stream exists underneath.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kafka</category>
      <category>flink</category>
      <category>dataengineering</category>
      <category>streamprocessing</category>
    </item>
  </channel>
</rss>
