DEV Community

Irvin Gil
Irvin Gil

Posted on

How I Shipped A Notifications Feature Using AI Agents as My Pair Architect

tldr: a walkthrough of how I used Claude Code and MCP as active tools at every stage of building a real feature — from researching knowledge bases to find existing architecture, soundboarding design decisions and e2e flows, analyzing a huge error dataset and generating human-readable translations, to using AI pair programming to write new functionality in Go. not a tutorial. a first-hand account of my own experience — what agentic developer workflows actually look like when shipping something to production.

key takeaways

  • using AI tools like Claude Code and Atlassian Rovo to query internal knowledge bases cut days off the discovery and research phase
  • soundboarding architecture decisions with an agent is a practical substitute for a pair architect when you're working alone
  • AI pair programming works for writing code in an unfamiliar language, but you still need to read, verify, and own every line it produces
  • cross-team coordination, reviews, and release still require a human driver — AI doesn't automate the people problems

the goal

we've got a worker-like syncing backend service that performs async data imports from a third-party HRIS — Workday, in this case — into our system. a customer raised a feature request: notify them when these async import processes fail. there was no existing functionality for this, so the impact was clear — the customer and our internal tech team would get notified of failures, action them accordingly, and stop losing time to issues silently churning in the background.

sounds easy enough. and this is how every feature starts — innocent and pure, with a clear problem devs are set to solve.

the hurdles and gaps

the most obvious problem surfaced almost immediately after writing the user story: "how do we send these notifications to the recipients? and in what form?" below, in roughly the order I can recall, are the questions I asked myself before doing any design work on how the feature would be implemented.

  1. "how can we send the emails to the recipients?"

email notifications were the obvious choice — it's the most common medium and the first thing that comes to mind. but the follow-up question hit right after: how, exactly, do we send them?

I didn't have deep knowledge of the full product suite, and asking senior members from other teams was the obvious option A — except that meant waiting. chasing answers across busy teams can cost you a day or three. luckily, I'd come across something useful: our organization had enabled Atlassian's MCP (Rovo), which lets local CLI-based agents query it for internal knowledge. that was exactly what I needed.

I spent a couple of hours learning how it worked and wiring it into my local Claude Code CLI. our org had also enabled Rovo on the Confluence knowledge base, so I used it directly through the UI as well — commissioning it to search existing software docs across the org for any existing email-sending architecture in the system abstraction I was working on.

turns out, there was one. the research surfaced an existing email-sending architecture built on an event-queue pattern. my plan: hook into it, learn the contracts and business rules, and just emit events to trigger the notifications.

  1. "how should the email notification look?"

this is the part I like the least about building this feature: UI/UX. my first instinct was that drafting the notification was a product or UX responsibility — not mine. but after floating it to the team, it became clear there were no resources for it. it was imminent that I'd have to scope and draft it myself.

I started doing research on what system-generated failure notifications look like in the wild. Datadog and CircleCI's pipeline failure emails were solid references, so I drew inspiration from those and hand-crafted a draft in a markdown file. then I ran it through an agent to soundboard the feel, tone, and structure of the notification body.

at first it was bare — just a summary of the total failures and successes from the sync, no data table, no granular detail. I presented it to the team and got feedback in a couple of hours: we needed to surface more information. specifically, a per-employee breakdown of who failed to sync and the reason why.

for v2 and onwards, I added a table surfacing the employee and failure information — raw HTTP codes and messages — directly in the email, no translation. I shared it for feedback and got pushback pretty quickly: it was too technical. anyone non-tech would be lost reading raw error payloads. the feedback was essentially: "409 - Conflict. existing resource already exists." needs to read as something like "Internal employee conflict discovered — the system can't proceed with a misconfigured entry." that opened a new sub-task: build a translation layer for these error messages.

and so I did. I spent a day enumerating failure reports from a full month of production data, then commissioned an agent to group all the error messages by structural similarity and produce a report. from there, I wrote and soundboarded generic human-readable messages for each group — with the help of an agent. 17 distinct error structures stood out. I added one more as a catch-all fallback for patterns that didn't match anything in the registry.

then I created a discovery page to get consensus on the design. feedback came in asynchronously over half a day — faster than I expected — and it got approved the next day I opened the doc.

  1. "who would build this? and how do we split the work?"

since the existing email-sending architecture was owned by another team, a joint effort made sense: we'd handle the report generation logic on our side, and the other team would own the template support for rendering the event.

by this point I'd already defined the plan and what the email would look like. no attachments — we'd shot down the idea of a CSV report stored in a cloud bucket and linked in the email. with the division of responsibilities clear, I set up a three-amigos meeting with backend engineers from the other team to lock in acceptance criteria and deliverables. more feedback came in during the session and asynchronously afterward. the outcome: they agreed to support a polymorphic object data structure in the event payload, and the consuming service would process it without error.

and the backend service that consumes and processes the notifications is written in GoLang, whereas I write Java code in Springboot. So the session to split the work and deliverables was much needed.

  1. "where does the recipient configuration live?"

I'd already had this in mind as soon as I discovered the existing email-sending architecture. my call was to keep the config within our team's domain and control, exposed via a REST API for CRUD operations. a few engineers pushed back — the existing architecture has per-recipient configuration for allowed email types, managed by a separate backend microservice. the argument came up a few times, asynchronously in the discovery doc. but I held the same reasoning: this config will be our responsibility to maintain and extend, not another team's. that held, and the config will live and be maintained in our domain.

scoping, discovery and soundboarding the solution

I want to zoom into how I've used AI — specifically Claude Code — to unblock myself, do research, and multiply work I'd normally be doing alone.

  1. research and discovery

this is honestly where it's saved me the most time. finding the right information in a software team's knowledge base takes time — getting used to where things live, filtering out deprecated docs, figuring out what's still relevant. I've already mentioned using Atlassian's MCP to let local agents query our online knowledge base. and while that's useful, per my experience the results can be thin or lead the agent toward inadequate answers.

for deeper research, using the knowledge base provider's own built-in AI has gotten me better output. sure, you end up with a wall of markdown when the report lands. but what I do is drop that into a file in the local service repo and point my CLI agent to use it as a reference during soundboarding and spec-building. that way the local agent is working with knowledge base context while also reading the actual implementation — and can make more informed decisions about the best end-to-end design.

  1. rubber duck and soundboarding

I've used the agent as both a sanity check and a pair architect when designing e2e flows. the way it works: I draft how I want the feature to behave and be implemented — even writing pseudocode in Java for control flows — then run a soundboarding session on the same file and instruct it to overwrite the contents with the decisions we've landed on.

it can be long and tedious, especially when you don't front-load aspects like throttling, idempotency, back pressure, and persistence behavior. the agent will surface all of these as the session goes on, which is a good thing — but the more comprehensive your initial write-up, the less time you spend in the session. it's basically a long /plan session with Claude Code 😂.

  1. documentation of existing features and functionality

a neat use of agents: point one at a service repo and ask it to generate a walkthrough of a feature or e2e flow, complete with Mermaid diagrams. especially useful if you're a visual learner, as I am. you can tune the precision — include method stack calls, branching logic, and the persistence model down to the database layer.

in my case, I used it to document how the consuming notification service processes the notification event: what types it supports, and how the branching and handling work throughout. I also asked it to produce a more code-comprehensive version of that document, which I used for spec-driven development on the service — more on that below.

the short: if the documentation doesn't exist in the knowledge base, an agent can write it. just point it at the repository and tell it what you need and how much detail to include.

  1. bootstrapping updates with new changes on the discovery document

I surfaced a discovery document on Atlassian Confluence for the proposals and feature designs — that's how engineering is done in our org. during feedback and review, changes are expected to happen on the same doc that will eventually become the source of truth for implementation.

the way I managed this: keep a copy of the discovery document as markdown in the local service repo and use git to track changes. the agent runs on the same repository and makes edits as I address remarks, update designs, and refine context. the loop is: soundboard and edit locally → publish the changes to Confluence → commit locally in an isolated branch. I repeat until the document reaches a high enough level of agreed decisions to start building. and even mid-development, when new feedback comes in, I use the same workflow.

approach and implementation: the planned happy path VS what actually happened

with all the major gaps filled and acceptance criteria defined, you'd think it'd be smooth sailing from here. maybe even let an AI agent one-shot the big implementation chunks. but I couldn't have been more wrong.

my projected happy path

in my head, with everything defined, it should all work fine:

  1. our team's implementation was clear and could be started in parallel with the other team's deliverables.
  2. the other team would handle the new template implementation — they own the service that consumes and processes the event.
  3. the translation layer I'd done discovery on should be enough to give both tech and non-tech recipients a clear picture of what happened.
  4. everything else in the email would be straightforward.

it sounds almost like a perfect developer execution plan.

what actually happened

I started implementing the feature piece by piece — the configuration, the cadence and triggering mechanism, the translation layer. not in the cleanest order, but I kept reviews manageable by breaking it into separate PRs and feature-flagging everything so I could merge without activating the full feature yet. from our team's side, that mostly held. there was some pushback from engineers wanting a full e2e merge, but I made the case and it landed as planned.

on the other team's side, it was a different story. our ask had to join their delivery queue — another 2–3 days before anyone picked it up. as soon as their related PR was open for review, I followed up with their manager asking if the new template work would be next. I got pushed back: what they'd agreed to was the polymorphic object data structure support, and the new template was not on their list of agreed deliverables.

I was unhappy with that. I spent some time deciding whether to escalate to my engineering manager or absorb it. eventually I chose to just own it: I was already using an AI agent for discovery, scoping, and soundboarding — why not take it further and write the template myself with Claude to finish the feature end to end?

I'd had success with agent-driven refactors before, but this was different. this was writing new functionality in Go — a language I had no professional experience in. I gave myself a pep talk: I'd done some Go on the side for fun, it shouldn't be a big deal. and so I started writing the template and getting my hands dirty.

I'll be honest — sorry if I'm moaning too much about being pushed to write Go code, but it was uncomfortable at first. the architecture was unfamiliar, I had to set up a dev environment to write and compile the service, and getting my head around the implementation and tests felt slow. it's a different paradigm. it felt like being a new joiner handed a key feature with a defined deadline. I pushed through one step at a time anyway.

then, to make it more dramatic, feedback landed on the discovery doc: we needed to add another column to the notification email table to clearly distinguish whether a row was customer-actionable or something our team needed to fix — a config error on our end. back to the drawing board to accommodate the new requirement, tagging every error I'd cataloged as either customer-actionable or internal. prioritization became critical here. keeping a running todo list and making commitments visible to the rest of the team was what kept me steady.

leveraging AI to write Go code

at this point, the PR for the polymorphic object data structure was still under review and not yet merged. I started work on the new template implementation. I briefly considered using GitHub's spec-kit tooling, but scrapped it — more than I needed. instead I wrote a bare-bones spec and approached it with Claude's /plan command, then worked through it via soundboarding.

I also put together a set of HTML files with another agent — each showing how a specific email scenario should look when it lands in the mailbox — making sure all scenarios were covered. edge case decisions like fallback behavior when a variable is null or an empty string, I captured those in the spec file separately from the HTML ones. it took 4–5 soundboarding iterations, but I eventually got a working spec that covered enough surface area for my use case — and more importantly, locked in the decisions on how the new template would fit into the existing Go service implementation.

before firing up the implementation, I needed a more focused soundboarding session on how the service would handle notification events from our product (Connect). the existing implementation has complex handling depending on which product a notification originates from. since mine was the first in Connect, I needed to establish the expectations and direct the decisions for the new flow (if product==CONNECT). I spent a couple of hours soundboarding that, then reading the Go code where the key handling logic lived to understand what it was doing. the agent was useful for getting a quick overview and plain-English explanation of the control flows — but I still needed to read and verify it myself to make informed decisions.

once the spec was in good shape, I opened a new Claude session and ran the implementation in agents mode to spread the work in parallel. it uses more tokens, but it lets Claude spin up sub-agents and delegate work across them rather than working through instructions one by one in a single thread.

a few minutes later the implementation agent was done with the template and logic. I spent the rest of that day and most of the next reviewing the output — reading through it, conventionally committing pieces as I went, and finally testing the rendered HTML template locally. then I opened a PR, and it failed the code-quality CI step: code smells flagged by SonarQube. not ideal when you're writing in a language you're still learning. I pondered how to give the agent enough context to fix them the right way.

I exported the Sonar report as JSON — pulled it from the browser dev tools by intercepting the API call to Sonar. it was a large file with a lot of properties that weren't relevant to what I wanted the agent to consume. I cleaned it up manually to keep only the actionable data, so the agent wouldn't waste tokens on noise.

then I fed it into the implementation agent: it now knew exactly which files and line numbers were triggering the code smell scans, along with context on how to resolve them. it took no more than 3 iterations. all code smells were resolved, and the code-quality step came back green.


once I had enough of the template implemented to open a review-worthy PR, testing became the next wall. the obvious move was to ask QA how they test against the DEV environment — but the timing was bad, much of the team was on holiday. no one with the right knowledge was reachable. so, with the agent's help, I set up a smoke test that sent real emails to our target test recipient. in parallel I built an isolated testing strategy: produce a manual payload for the scenarios our service was projected to emit → send it to the notification queue → verify the notification service processed it with the new template → confirm it landed in the target inbox.

did I wait for the PR to be approved before testing? no. as soon as I assessed it was review-ready and the todos were ticked, I cracked on creating e2e snippets and fixtures. I did wait for the polymorphic data structure support to be released first, and my testing followed shortly after. my reasoning: waiting would mean missing the deadline. I still made sure no regressions were introduced — just didn't wait on someone else's sign-off to keep moving.


fast forward past the merges on our team's domains. the only thing left was the new template PR on the consuming notification service. it was blocking the release. I escalated to our team manager — I'd hit the limit of what I could chase alone.

eventually, someone was assigned to review. even then, the repo required approval from a member of a specific senior engineer group, so we had to chase that as well. and while that was in progress, the reviewing engineer flagged that QA needed to regression test against my DEV deployment. I explained QA was short-staffed with the holiday period. my manager asked if I could run the tests myself. I was hesitant — not my role, and I wasn't sure I'd cover it well enough.

but I thought: if we wait for someone to be back next week, we miss the delivery. so I spent half a day figuring out, setting up, and running the regression tests targeted at the service I was changing — no AI agent, just the project readme. luckily it was a Java/Maven project (Cucumber, I think — the tests were Gherkin-based), and all tests passed with no failures.


from there, it took another day to release and configure the sync notifications. I had to collect target mailboxes from the customer → create accounts for each → configure them per tenant manually by calling the exposed endpoint on our service, 37 tenants in total → then cross my fingers and hope nothing exploded in production when the first real notifications came flying.

what i've shipped

after all the churning — chasing reviewers, accommodating mid-development spec changes, running regression tests — the feature was live. notifications were sending, the report was generating, and both our internal team and the customer had clear visibility on failures with a direct indication of who needed to act to fix them.

the feature landed well and generated more interest from the customer who'd originally requested it.

even after that, the customer team came back with a few tweaks. minor ones, but we held the line on scope — the feature solves one category of problem and we weren't going to let it balloon. for me personally: I'm glad I pushed through it. the ambiguity, the deadline, writing Go in a foreign codebase — getting to the other side of all that felt good.

I can't share the detailed architecture, but here's a simplified diagram of what we built on our end. the real version was considerably more involved — the translation layer uses a strategy pattern to match all known error structures and translate raw HTTP error codes into human-readable messages.

and here's what one of the failure notifications looks like when it finally lands in the target mailbox. today the feature is doing exactly what it was built to do, with a few improvements still in the backlog. it has meaningfully cut the time internal teams spend pinging us to inspect failure details from the async import process — giving back time that used to go toward operational support instead of actual development.

looking back and internal retrospective

my experience building this feature wasn't all sunshine and rainbows. but I want to highlight what worked — specifically, what AI and agentic workflows contributed across discovery, scoping, development, and release.

  • use AI as a tool, not a decision-maker. key architecture decisions and trade-offs should stay with you. knowing which solution is good enough and what's actually worth building is what separates engineering from just programming. AI can speed up scoping and decision-making, but don't offload the judgment itself.

  • context is everything — give the agent access to it. an agent is only as good as the context it can reach. there are valid concerns about what it does with the capabilities you give it, but those can be managed with guardrails and a clear constitution (a CLAUDE.md). enabling the agent to access information directly improves output quality. if you can't do that, grab the information yourself and drop it in a local markdown file.

  • never skip the planning step. it's always been tempting to skim rather than read the full spec the agent produces after /plan. I've learned that a low-quality plan produces a low-quality outcome. AI can be a good pair programmer, but only when given the right instructions — so read the plan, correct it, and make sure it reflects what you actually want built.

  • use AI creatively alongside your existing tools. I've talked about pairing AI-generated specs and notes with git to track what changed. this comes in handy because AI generates a lot of text — without a way to see what's shifted, you lose track of what was tried and what was rejected. find ways to wire AI into the tools you already use.

  • AI can't manage feedback or hold a line on deadlines — people can. at the end of the day, it's still a tool. feature roadmaps and delivery dates are driven by people, and it takes people to push back and negotiate what's worth building. I experienced this directly on this feature — it was pushed to do more than originally scoped, more than once. it's still on us as developers to ask "why?" and challenge requests that don't make sense. even as AI takes on more of the software development lifecycle, the important conversations remain human.


I've learned a great deal from this experience. I'm still figuring out what strategies and workflows fit my day-to-day, and I like to understand a technique or tool before committing to it — what problem does it solve? is it worth the time to get good at it?

this is all a work in progress. but I've laid out my full thought process here: how I approached building the sync notification feature, the challenges I had to push through, and where AI actually helped move things forward. if you're in a similar position — trying to ship something real with agentic tooling — learn it, use it intentionally, and figure out what it's actually good at. it really does multiply what you can do as a developer.

Top comments (0)