Can AI be trusted to help build backend services that handle financial transactions? We had doubts. We also had one quarter to ship something that would traditionally take two or three, so we went on the journey to find out.
We introduced AI as a first-class collaborator in every phase of the development life cycle: refining product requirements, domain exploration, system architecture, Jira backlog creation, detailed implementation plans and finally code implementation and review. AI was heavily involved throughout. During execution we let it write almost 100% of the code, while human review and oversight remained critical. We wanted maximum velocity, but not at the cost of ending up with a vibecoded codebase that worked by coincidence.
The magic of vibecoding is that you can get something that looks like it works quite fast. But under the surface we kept noticing edge cases in the initial plans that could quietly break things in production. AI generated a plan that would have worked 99% of the time but would leave financial transactions inconsistent during database downtime. Something easily avoidable, but the kind of flaw you only catch if you fully understand the design. This is what Andy Hunt and Dave Thomas called Programming by Coincidence in The Pragmatic Programmer — relying on luck and accidental successes rather than understanding why code works. Better context and prompts help sometimes; other times there’s no magic bullet — yet.
The partnership works when we do the thinking and AI does the execution, not the other way around.
That said, we didn’t reject vibecoding entirely. It earned its place in non-critical parts of the codebase: internal tools to speed up QA, CLI tools, quick proof of concept code and plenty of other places.
So what do we actually do?
Our team builds services that help hoteliers consolidate their banking operations. Our first goal is automating the reconciliation of wire transfer payments — eliminating the daily ritual of manually checking the bank account, figuring out who paid what, and matching it inside the PMS.
We shipped a first iteration (V1) last year. It worked, but we knew the limits going in: a small team still forming, a tight timeline, product compromises accepted on purpose to gain domain expertise and crucial feedback.
This time we had new superpowers available:
- Stronger than ever AI models
- Mature coding agents
- Claude Code evaluation period with a very generous token allowance
Phase 1: The foundations. Curating Context for AI with AI.
Before AI wrote a single line of production code, we used it to set up the foundational context of our solution in markdown files that became part of the codebase.
Business Context
The first ingredient was a clear definition of the problem we were trying to solve. BusinessContext.md was a curated AI distillation of investigation spikes originally written in Confluence pages by Software Engineers, Product Manager and Product Design — capturing what the problem was, how we were thinking of solving it, and the rough scope based on what our external banking provider could offer.
It became one of the very first files added to the repository and the guiding document both for team alignment and for AI to understand what it was actually working on.
External Banking Provider documentation, flows and examples
Next we mapped out every external banking provider flow that would be part of our solution — API endpoints with example requests and responses, plus all the webhook payloads we needed to track. AI helped us quickly turn the provider’s documentation into Postman collections, and we vibecoded small CLI tools to test flows against the sandbox and confirm things actually worked the way we assumed.
Having an accurate picture of what the provider could offer and its limitations was critical context for planning the solution.
Solution architecture direction and v1 to v2 evolution
Before developing with AI, we did a team retrospective on the v1 decisions: solution architecture, design patterns, code conventions. The new service would have to be built on a completely different provider model. That meant 75% of the code would be greenfield. And since v2 would need to live alongside v1, we also had to solve the difficult challenge of how to evolve v1 into v2 without disturbing the v1 production environment.
We pointed AI at v1 and tasked it to produce a structured comparison: architecture, design patterns, testing approach, library choices, resilience configuration. From there we brainstormed the v2 direction — what to keep, what to change, what was still TBD — and reviewed the trade-offs.
What would have been hours of tracing through layers and documenting happened in a fraction of the time.The team peer-reviewed the document and made the final calls.
Deterministic code quality guardrails
The second decision was agreeing on a strict code-quality ruleset before humans or AI wrote any code. We employed multiple complementary analyzer packages popular in the .NET ecosystem that would reject the build on violation until fixed — 90+ active rules with zero suppressions without documented rationale. Our philosophy was “start strict, relax later.”
In the era of AI code generation a strict code quality setup becomes the only deterministic guardrail to enforce consistency, avoid common bad practices and cap method complexity at sane thresholds. Traditional strict code quality gates used to be an annoyance for human programmers. I doubt they bother Claude too much.
What Phase 1 produced
By the end of Phase 1 the repository already had the context documents that everything else would build on:
- BusinessContext.md — the problem and how we are thinking of solving it
- ExternalProviderApi.md and ExampleRequestsResponses.md — endpoints, flows and webhook payloads
- ArchitecturalDecisions.md — v1 to v2 comparison, evolution, trade-offs and final calls
- CodeQualityStrategy.md — analyzer rulesets and rationale
- Postman collections and small CLI tools for sandbox verification
Phase 1 was heavily human-driven and although AI wasn’t a massive productivity leap it still moved us through the process at a faster pace. As we progressed through the phases that balance shifted.
Phase 2: Domain Discovery and initial implementation plan
With Phase 1 solidifying all the major context documents, we had what we needed to create an initial implementation plan for how v2 would be structured.
We fed that context to AI and used it to draft a comprehensive implementation plan — the one decision in the project that initially looked most like a waterfall anti-pattern. The ImplementationSummary.md was a detailed upfront specification of the entire solution: project structure, every aggregate with its entities and value objects (4 in the end), every use case with its command/query signature (12+), every webhook handler (10+), the provider abstraction design, the outbox pattern, query specs, testing approach and more.
The document was heavily reviewed and reprompted, but initially we did not bother too much with nailing all the implementation details. We wanted to avoid the most common pitfalls of a more strict spec driven design and accept a more pragmatic approach by embracing some ambiguity and unknowns. There was a good reason the waterfall approach was abandoned — for complex enterprise software, good solutions are still broken down, built and tested incrementally, not generated in one huge vibecoded chunk.
The Implementation Summary did three things, none of which were meant to nail every little detail upfront:
- AI context anchor. In every conversation, AI could reference the ImplementationSummary to understand the full picture: where we are, where we’re going, and how the backlog being worked on fits into the whole. Without it, each session would start with AI having to rediscover the project’s shape.
- Living document that adapted to reality. As implementation progressed and new challenges emerged, the document evolved. For example we had to adapt some of the design when a domain aggregate made sense to be split into two, or when we discovered that some design decisions conflicted with our goal of an agnostic banking provider abstraction. The document adapted to the implementation, not the other way around.
- Guide for sprint planning and backlog creation. The ImplementationSummary became the source for JiraTicketBreakdown.md, which was then used to draft a backlog plan with sprint epics, stories and rough estimates, and finally to help create the actual Jira tickets. Each smaller backlog was then reviewed, refined and given its own detailed but flexible implementation plan.
By the end, the Implementation Summary had transformed from a plan into the actual documentation of the codebase.
Final adjustments
Before we started implementation, and considering we would be working mostly with Claude Code, we created a concise CLAUDE.md that referenced all the relevant documents from previous phases and set some basic context, guidelines and guardrails. We kept it up to 200 lines. The discipline mattered: a bloated CLAUDE.md becomes noise. We wanted something that pointed AI at the right detailed documentation rather than trying to inline everything.

We also created various skills, commands and subagents as we went. The most notable was a code-review subagent that evolved alongside the project. It started as a simple generic prompt and grew over the quarter into something opinionated about our specific architecture, design decisions and context. Every time review agents running on our CI/CD pipeline exposed gaps in our review subagent, we improved it so similar cases would be caught early.
It was crucial to keep updating CLAUDE.md and every other prompt that loaded on demand, so any mistakes or gaps identified in AI output would not resurface later.
This was the process of moving from generic prompts like
"You are a super experienced senior software engineer applying CQRS, DDD and Clean Code, make no mistakes"
To something like this
“Webhook Handlers vs Use Cases — Separation of Concerns: Webhook handlers are thin adapters: extract data from the webhook payload and dispatch a command to the use case. They should not perform input validation (e.g., IsNullOrWhiteSpace checks) — that is the use case's responsibility via Guard Clauses. Use case command handlers own all validation (Guard Clauses for exceptional/programming errors, Result pattern for business errors). This keeps validation logic in one place and prevents webhook handlers from duplicating concerns that belong to the use case layer.”
Phase 3: Rapid Implementation
In every phase the effort we had put in the previous one was paying off for the next. The ratio between human and AI effort was shifting rapidly toward AI. If Phase 1 felt mostly human with AI assisting, Phase 3 felt the other way around — all of the code typing was done by AI and most of our time went into prompting, reviewing and making sure we understood what was going on.
This phase was all about breaking down the ImplementationSummary into smaller refined plans and building the solution in small increments.
With the domain validated, the architecture peer-reviewed, the banking provider documentation curated, all guardrails in place, and the Implementation Summary as a north star, Claude had everything it needed to produce production-quality technical plans and code.
The workflow we settled into for each backlog was:
- Refine the backlog item from JiraTicketBreakdown.md and create the Jira ticket
- Feed it to Claude Code to generate a backlog-level technical plan
- Review and fine-tune the plan
- Implement the plan with Claude Code
- Review the generated code
- Prompt for any changes
- Run the code review loops on the local subagent until clean
- QA in local environment
- Raise pull request
- Address human and Copilot AI review agent feedback
- Merge and QA in development environment
The compound effect during this phase was real. Once the first building blocks were in place, everything became gradually easier and faster to implement.
The time saved prompting code instead of manually typing it is what truly accelerated development. This workflow let us go faster without sacrificing code quality, control and understanding. AI became a productivity multiplier that helped us plan and ship code faster than ever before.
Risks
Despite all the effort, AI does not deterministically follow every rule and convention, and it does make mistakes. AI-generated implementations, especially on event driven architectures and complex business logic, need close human review combined with AI review feedback loops focused on data consistency, idempotency and graceful handling of failures. There were cases where the initial AI implementation missed the mark on those areas, made false assumptions or even hallucinated webhook bodies and HTTP responses.
For example, early in Phase 3 we had to intervene on an AI generated plan that saved a financial transaction in our database after it was initiated on the external provider. This would work 99% of the time and, to an external user, everything would seem fine. But if our database experienced any downtime, we’d end up with a major data inconsistency, no record of the transaction on our side and no easy recovery strategy. For this and similar cases we have various strategies in our solution like saving the intent first with a deterministic idempotency key, gracefully recovering from duplicate requests, unit of work and outbox pattern. Every case has an appropriate strategy. Once those are in place or explicitly stated during design prompting, AI can indeed produce good solutions.
A different kind of example was when we lacked documentation for a webhook payload. Instead of flagging the gap, the coding agent went ahead with an implementation based on a very convincing assumption of what the body would look like, extrapolated from the other webhook payloads we had. The assumption, although close enough, was obviously wrong.
On other occasions, Claude Code straight up ignored rules clearly defined in CLAUDE.md.
Better context and prompts reduce these risks by a lot, but blindly trusting AI output without judgment is the biggest risk we identify at the moment. Models and coding agents are still evolving rapidly and that often means new bugs and random regressions slipping in along the way.
Learnings
Some weeks ago our service hit production and processed its first successful transactions — two weeks ahead of schedule, with ~8 weeks from empty project to payouts running through an integration event pipeline. The team embraced AI and pulled real productivity gains out of it. A few principles stood out as ones we’ll carry into future work.
AI does the breadth, humans provide depth and judgement. AI excels at scanning, comparing, drafting, and replicating patterns across a large surface area. Humans excel at judgment, trade-off evaluation, and spotting edge cases. Together they’re a productivity multiplier without the slop.
Upfront investment compounds. The effort balance shifted continuously from humans toward AI as we moved through the phases. The solid foundations laid early are what let AI become a real multiplier during implementation and review.
Plan upfront, adapt continuously. The ImplementationSummary looked like waterfall but functioned like agile — a north star that gave AI and humans the same big-picture context, evolving with every design decision. The discipline was keeping it in sync, not treating it as frozen spec.
Writing the plan down forces alignment. Regardless of how accurate the final plan turns out to be, the act of curating it gets engineers, PMs, and AI on the same page before code gets written. Even when the plan evolved heavily during implementation, that initial shared map was one of the biggest payoffs.
Multiple review agents complement each other. Our local review subagent had better context and produced more accurate findings, but GitHub Copilot’s CI/CD review caught gaps the local one missed. Together they gave us a layered confidence we wouldn’t have had from either alone. Copilot pushes for overly defensive code, but editing AI-generated code is cheap enough now that the extra verbosity doesn’t slow us down.
Final thoughts
So where do we go from here?
AI has been a massive productivity multiplier, especially during execution, and helped a lot in the phases before that. We took the time to build the scaffolding properly and let AI do the parts it’s actually good at.
AI development evolves fast and our spec driven hybrid approach probably already feels outdated as I write this.
Despite all the hype about new AI tools and models, the same core engineering skills still apply. Understanding the domain, articulating the problem, breaking system design down into executable plans, having the judgment to decide what’s crucial for the model’s context — that’s what makes AI look like magic.
A big concern I have is that there’s a tendency in the industry to talk about AI adoption as if all software were the same. It isn’t.
A landing page, an internal admin panel, a simple frontend on top of an API — these are places where more vibecoding style approaches genuinely shine. The cost of being wrong is low. If something breaks, you fix it. If the code is ugly under the hood, no one pays for it. Letting AI run with minimal oversight is often the right call.
But for a backend service in a distributed system the cost of being wrong isn’t just a bug — it’s a transaction that vanishes, a reconciliation that silently drifts, an audit trail with a hole in it. You don’t get to fix that with a hotfix on Monday, and if you have no idea what the codebase looks like by that point, well… good luck prompting your way out of it.
There’s no doubt the future is moving toward more AI-native workflows. The evolution of software engineering to agentic engineering is what shapes our roles. Continuously skilling up the agents, prompts, context and tooling to mitigate the risks that come with those workflows is what unlocks real productivity gains — by letting teams hand over more control with confidence.
But fully stepping back from the responsibility to review and understand the code? For certain parts of the software stack, I think we still have quite a lot of work to do.