Skip to main content

Two years of change: how AI reshaped software engineering at a small company

Updated 17 min readMarkdown

When I joined smAily in 2024 as a backend developer, I spent almost 100% of my time coding. In August 2026, I developed the new version of our core feature without writing a single line of code myself. ​ I’m Michał Gutowski, a solo backend developer at smAily. This article shows how smAily's development practices changed over the last two years. From using chats and autocomplete, through first unsuccessful attempts with a coding agent, to code fully written by an LLM. What is smAily

SmAily is an app that lets users record meetings and create summaries from those recordings. We focus on in-person meetings, where one participant records the meeting using their phone and uploads the resulting file to smAily’s backend for processing. ​ From the organizational perspective, we are a fully bootstrapped company registered in Switzerland. We work fully remotely. ​ Under the hood, smAily consists of a mobile app and a backend that uses LLMs to summarize meetings. Users record the meeting on their device and send it to the backend for processing. More specifically, processing consists of a transcription step, where we convert audio to text, followed by a summarization step, where we summarize the transcript. ​

How things worked before

I joined smAily at the beginning of 2024. At that time, the company had a clear idea for the app: it had to keep user data private while still being easy to use. A software house had already built an initial version of the app, but unfortunately it was very low quality. I took over as the main developer and balanced refactoring with feature implementation. It was my side project, and I was working on it outside my main working hours. Back then, the main bottleneck was coding. I had roughly 2-3 hours a day, plus a bit more on weekends, to code, so I had to focus on the most important aspects. There were some rough edges. Some parts of the code could use polishing, and I had a big backlog of nice-to-have items. I worked on the backend alone and relied heavily on TDD, type checking (mypy), and linters (bandit and ruff) to avoid breaking anything. At the start of development, I built a reliable CI/CD setup, which sped up development. In the end, we released the app in early 2025. ​ ##LLMs in the early days of smAily: chats and autocomplete

The smAily team was never particularly big. Since efficiency was crucial, we relied heavily on chat and AI autocomplete. I used ChatGPT and - for a limited time - a JetBrains AI subscription. ​ Even back then, the chat was already very useful for debugging and minor development work. It offered tremendous help with writing the procedure to generate docx files - using the Python docx library is quite difficult if you want a more complex layout. Still, the chat was somewhat inconvenient to use: you had to copy your code, then paste the output back. The whole experience improved noticeably with JetBrains AI in my IDE. ​ I used autocomplete quite extensively with mixed results. Sometimes, the code it suggested seemed plausible at first glance but, in fact, contained serious errors. ​ My first experience with agentic coding using Junie from JetBrains was really disappointing. Even simple tasks took a long time, and in the end the code was messy and buggy. After my initial experiments in June 2025, I decided not to use agents for coding. ​

The Turning Point: Business subscription service, the first fully agent-coded piece

In January 2026, smAily launched a business subscription service that let business customers buy one subscription for multiple users. With this subscription, the customer could manage access to the app via a dedicated portal. This was a perfect chance to try Claude code with Opus (I think 4.5). I had already experimented with it during the winter break and was really amazed by the results. Still, I wondered how well it would handle a more business-oriented project. ​ The app ended up as a Spring application with a React frontend. My main reason was to use Kotlin's type safety and some ready-to-use Spring tools, like Spring Data auto-generated repositories. In hindsight, using another tech stack made no sense, but at the time I thought it would be much faster to use Spring's built-in CRUD features and a built-in Spring scheduler. In the end, I wrote everything using Claude code, so the stack probably didn’t matter much. The app itself is basic: AWS Cognito handles authentication, which integrates well with Spring Security; there is a small database; and there are a couple of HTTP-based integrations. In general, I assumed it would take a couple of days, but it took about 10 working hours to turn an empty repository into a service running in production. ​ When it came to working with the agents, I was doing small iterations, starting with a walking skeleton - an app that had only API endpoints exposed and returned hard-coded data to wire the frontend. Next, I gradually added persistence and real API calls. ​ Due to my limited design skills, I asked Claude to copy the style of our landing page (http://smaily.ai/). The landing page was created much earlier by real designers, so it was a good baseline. The initial version blew me away - I wouldn't be able to create anything similar myself. Of course, some minor aspects weren’t in line with our design (e.g., fonts, some colors), and one section wasn’t sufficiently readable, but overall the output was good enough. Also, we didn’t need a mobile version of the web app. ​ That was the moment I realized I really want to use coding agents in my development. ​

The new approach case study: Better minutes

In July 2026, our team decided that we needed to introduce something new. Models had really evolved over the past two years, and our initial approach to creating minutes wasn’t good enough anymore. The idea was to let users define their own templates and send their own agendas. We didn’t want to stop at visuals; we wanted users to feel like they were working with artificial intelligence. Our goal was to use agents to understand the templates and adjust the output to the content sent by the users - and this meant changing the whole approach. ​

Our plan

The idea was to create an entirely new set of endpoints with separate app models for the new processing. We needed to allow users to send pictures of their company templates and attach optional agendas. Initially, I wanted to extend the current flow, but since I wasn’t constrained by coding speed, I decided to build a new set of endpoints and services completely separate from the old flow. ​ For the whole development cycle, I used Claude Code with the Max plan. It was more than enough for the task. For models, I mostly used Fable, occasionally switching to Opus for smaller tasks. ​

Groundwork

For the backend, I started with an approach similar to the one used for the business subscription service - first creating a walking skeleton, just a couple of endpoints with hard-coded data. I quickly realized I didn't need to constrain myself that much and could implement real persistence and basic summarization. For summarization, I used a very basic approach: a simple "summarize that meeting" prompt. First, I wanted to build a reliable communication flow without getting into too many specifics about LLM processing. ​ Claude code initially implemented a very naive flow. There were issues with transaction boundaries - data was written to the DB when the email was still to be sent. Even though our scale is much smaller than Google’s, something could still go wrong, and since the backend is a one-person job, reliability was critical. Fortunately, when I pointed Fable to use patterns like outbox, it was able to design the feature the way that I wanted. ​

Loops and prompts

As you can imagine, the output of a simple "summarize that meeting" prompt wasn’t what we actually needed. The same was true for template generation: the documents our app created differed significantly from the uploads. We needed to improve the outputs. ​ In parallel with the development, while I was laying the groundwork, our CEO Bertrand created several test cases. He used Claude to generate sample meeting scripts (scripts for actors, not bash) and user expectations (e.g., a meeting should mention the place Gruppenraum 3, Bibliothek). Of course, we didn't use any real customer data, but we tried to make these samples as realistic as possible. Bertrand came up with different kinds of meetings. He made sure to include some potentially challenging content - for example, during one of the meetings, at one point somebody would say that the budget was 136 000 CHF, but would later correct himself, stating that it was actually 163 000 CHF. Each test case set included a transcript (we didn’t want to test our product's transcription feature), an expected template, and an Excel file listing all the important points to check. ​ With the test data ready, I started developing the core of our product. I treated the whole process as an optimization task; the product needed to pass as many test cases as possible. I asked Fable to generate a harness - a script that would load the Excel file with expectations - and then used a different LLM to judge the output. For observability during development, I used Langfuse, which comes with tracing and a human review queue. I reviewed the traces, while Bertrand used the human review queue. I asked Fable to develop the prompt and change the code as needed so that it would pass more and more test cases, and it succeeded. The changes clearly showed how the code analyzed the output, identified missing or incorrect information, adjusted the prompt, and reran the process. After a couple of hours, we ended up with a very solid result. ​ While working like this, I encountered a prime example of why the output has to be reviewed. The harness was a throwaway script we wouldn’t use in production later, so I didn’t pay close attention to what it was doing. Since the script produced plausible results, I didn’t check the details. Bertrand realized we weren’t testing any test cases with an ID longer than 1 digit. This simple bug resulted from developing the harness with only the first test case set available, which didn’t include any cases with an ID longer than 1 digit. To be honest, I doubt that I would introduce such a bug if I wrote the harness myself. ​ While developing the code responsible for generating templates from images, I hit a local maximum issue. At first, I asked Fable to generate a routine to create docx files from an image. Fable went with an approach that generated a couple of XML files and zipped them together (you can read a bit more about the format itself here: https://gist.github.com/felipeochoa/81d8fa27901e8222c6ffbeb165a85acc). The app has a few loops to validate the LLM output, and after implementing Fable's initial approach, those loops spent most of their runs detecting basic issues like invalid XML. I asked Claude to improve the process; it shuffled parts of the prompts but didn't produce any visible improvement. At that point, I changed the approach: instead of asking it to improve the XML generation, I asked Claude to generate a routine that produces python-docx code, which ultimately worked. Of course, we needed to change the strategy when it came to running the code - I didn’t want to run LLM-generated code in the same container with processed customer data. I think this is a prime example of the need for a human in the loop. If I unquestioningly trusted Claude’s ideas, I'd just be burning tokens and going nowhere. ​ In general, I was amazed by how much you can achieve by simply asking Fable to improve the output and giving it tools to verify whether the changes are going in the right direction. You have to ensure that the harness is really checking the required elements and review the output. Still, the whole process was orders of magnitude faster than the initial development 2 years earlier, and it could run without my constant supervision. ​

The outcome

From the backend perspective, developing the new app version took around 50 work hours over 3 weeks. I doubt I could build it that fast if I had to write everything manually. ​

Working with agents: workflow, sandboxing, review, and skills

Before I start developing any features, I think through what to implement and usually have at least a rough idea of how I would build it myself. I usually have initial chats with Claude, weigh opinions, and ask for different approaches. I implement things gradually so I can fully understand them. At the beginning, I always ask Claude Code to prepare a plan, then review it. Often, I cannot really imagine the code changes it proposes. Still, a high-level overview is usually enough to spot obvious issues early and avoid wasting tokens on unwanted solutions. ​ I didn’t feel comfortable letting Claude run unguarded on my computer, especially when running the harness, which could delete files, install software, etc. My approach is to use a VPS. In my case, I didn’t need anything fancy - 16 GB of memory and 4 CPUs were more than enough. I used GitHub CLI with the token just for the repository and started Claude using claude remote-control --spawn worktree. For me, the web interface was convenient enough. ​ Working with smaller chunks made code review much easier. I also had a good mental model of how the whole process should run. Before the code review, I used https://github.com/VirtusLab/tulip to generate diagrams and make preparations. Whenever I didn’t understand something, I would discuss it with the LLM to better understand the underlying concept. While the backend still wasn't released to prod, I periodically asked Fable to find issues in the current code. In most cases, I used this solution as a hand-triggered nightly run whenever I still had some unused tokens. There were plenty of nitpicks, but in general, Claude found a few issues I had missed in my reviews. It also found some issues in the older, handwritten code. ​ At first, I used the comment feature in the GitHub UI and called @claude, but it led to multiple sessions competing against each other. Afterward, I moved to dropping the feedback straight into the chat, which worked very well. ​ For skills and instructions, I didn’t use anything fancy. I only made a couple of global rules, for example, to use Pydantic instead of dataclasses, provided instructions on how to run the tests and lint, and instructed Claude to run this set at the end of every run. The only skill I imported was https://github.com/DietrichGebert/ponytail/tree/main, which I chose after reading the following article: https://blog.jetbrains.com/ai/2026/07/ponytail-skill-claude-tested/. I don't have a good comparison of code written with and without this skill enabled, but I can attest that the model created very reasonable code without over-engineering it. ​

Key changes over the 2 years of development

The code is no longer a bottleneck. As a solo backend developer, I feel empowered because I can generate code at a pace that I couldn’t imagine 2 years ago. It's also much easier to start development: I can ask the model to generate an initial offering, then take it from there and instruct it on how to improve the proposed solution. Changing the code got much easier. Although you still have to be cautious about persistence, business logic, and security, it's much easier to move parts of the code around and much cheaper to implement features properly. Now, I don't need to take any shortcuts. ​ What is more, loops are a very useful solution. A model can go a long way if you give it the tools to validate its output. This is the most considerable change. Before the LLM era, running such experiments required careful oversight; nowadays, you can ask the model to try something and improve it. Still, you, as a human, need to actively validate both tests to ensure you focus on the crucial elements and thoroughly check the output. LLMs tend to optimize what they already have without trying something entirely different once they hit a dead end. The human developer should monitor where the process is going and adjust it once the model starts going in circles. ​ For me, the backend code review doesn’t pose a problem - I am not bombarded by sloppy code that I need to review. I regularly interact with the model and have a good idea of what should be implemented and - more or less - how it should be done before I write a single character in the Claude code chat. Occasionally, I generate a data-flow diagram to ensure my understanding is still aligned. In my opinion, this is the biggest change; previously, I was sure about the whole flow because I was the only person writing it. Currently, I often have to relearn and revalidate the flow - and even though I develop the code part by part and regularly review it in its entirety, a couple of times I was surprised to learn how some solutions worked. ​ Surprisingly, I realized that the value of test runs in the CI decreased. When I was working alone, test failures on CI were quite common - I often forgot to run the whole suite before pushing. Once I started using Claude Code and instructing it to always run tests before pushing, I never saw a pipeline fail because of tests. On the other hand, the CD part has become more relevant. When I developed the code manually, I could easily deploy it manually. Now, when agents do most of the work, it's much easier to merge changes and wait for deployment. ​ Since I'm using the subscription and I'm not billed for token usage, I sometimes feel that if I don't use the limit fully, I waste tokens. I would usually run chats to validate the current implementation. Still, sometimes I would also run smaller experiments, for example, asking Claude to change parts of the code and run the harness. Most of these experiments didn’t yield meaningful results, but it's now much easier to test ideas. ​ On the other hand, I find it much more difficult to escape work - it feels so tempting to check my phone to see how the harness is going or ask Claude to implement a new solution. ​

Conclusion

​ Even though I used LLMs from day one at smAily, the whole flow changed significantly. At first, I only used the chat, but recently an LLM wrote the whole code. This solution offers plenty of possibilities - writing code is no longer a bottleneck - but at the same time it comes with new challenges like understanding and reviewing the code. I can see that you trade some code understanding and control, but from my experience, the return is worth it. The sheer number of possibilities agents offer me as a solo developer is astonishing. I can bring much more value in much less time. A solo developer's perspective can be very different from that of people who work on bigger teams. I don't have much of an issue with code reviews, and from what I read, this is a common issue at bigger teams. Even though coding agents aren't perfect, and you still have to spend effort understanding the code they create and reviewing it carefully, I'm not going back to writing the code by hand.

No comments yet. Be the first to comment.