Tech Leading—A Story of One Project in Four Acts

Mateusz Karbowiak


This is a story of how a day-to-day developer overcame his fears and came out of his comfort zone. Of how he and his team got into a project which code was so bad they cried a little when they saw it for the first time. Of how they took care of it and made it great. Of how they made clients happy and left the whole team with a “mission accomplished” feeling. Of how they finally succeeded.

Act one—The Audit

Several months ago, Magda (my dev colleague) and I were informed about a new project starting in our company. That’s always exciting. We were supposed to dive into it and analyze its primary requirements. We learned that the client requested implementation of two main features into the product—payments and some kind of analytics. Enough to compliment the MVP and making it production-ready. Easy-peasy, we thought.

The reality was much, much darker… The existing styles were completely off and everything was overlapping. Server logs were stored in the repository. Data of some views were hardcoded. We even found a method 73-lines-long, where the 10-15 is a good practice. Login form was accepting email and password but the controller was only fetching User by the former and creating a session. Passwords were stored as plain-text.


We came to a decision—we didn’t want to implement any new features in the application so harmful to end-users. There was only one thing we could have done: perform an audit and summarize it in an accessible way to our client suggesting improvements to the existing product. It was, in a way, a whole new experience for me, but with a support of my colleagues completely feasible.

We divided our work and document into three parts:

  • User experience - quick exploratory testing, going from page to page and using the app as a regular user, writing down and screenshotting all the bugs;
  •  All repo code review - we forked a repository and thoroughly went through the code as we do every day (but on a much smaller scale—per pull request) commenting on potential bugs, design issues, and architecture;
  • Summary and “Monterail’s best practices” - gather all information from previous points and confront them with our best practices used in other projects.

A lot of good came out of this.

Conducting the audit helped us quickly understand most of the business logic behind the project. We could also provide our client with a complete report of existing problems and prepare a plan to kickstart the implementation using good practices and new patterns.

Act two—The Technology

Based on audit’s outcome together with the client, we decided to rewrite the app from scratch. As client spent some time and money on the project already we had to make it as efficiently as possible to catch up.

As initially, I was alone in the project for some time, we went with the easiest setup to minimize the risk of mistakes. Basic Rails app (non-SPA) for backend + Webpacker with Vue as our front-end library. We use standard Rails controllers and server-side rendering for static data. Vue for reusable parts (components!) with Ajax calls to API in more interactive parts of the app. After several months of development, I must say it works pretty great.

Along the way, the project gained one BE developer, one FE developer, a QA expert, and a designer.

Our team

Client clearly saw that initially broken project can be reforged to something great. The product started to look good both visually and code-wisely. Thanks to our weekly in-house “Software Architecture” meetings and following SOLID principles, we were able to introduce some neat patterns to our app. Goodbye, bloated controllers and models! Goodbye services catalog with over 9000 do_everything.rb files! Welcome, use cases and repositories! Welcome new abstraction layers, single responsibilities, and cleaner code!


Repositories helped us move all ActiveRecord calls into single files (per model or context).

# repository
class UserRepository < BaseRepository
  class << self
    def scheduled_emails
      # ActiveRecord or SQL calls to retrieve some data

# usage in code
scheduled_emails = UserRepository.scheduled_emails

Benefits? Thin models. Reusable code.

Every time we need to change some returned data, we go to one place. Every time we find a nasty N+1 query, we change it in one place. That also made writing tests a lot easier. We only check if a repo received a call. The only specs that hit the database are those for repos.

# code
scheduled_emails = UserRepository.scheduled_emails

# spec
it { expect(user_repo).to have_received(:scheduled_emails) } 

This sped up our tests. A lot.

Tests speed and results

I know that this is not exactly repository pattern “by the book”. That they should interact with the database directly. Here, it’s still model’s responsibility. Actually, those repositories are only another layer on ActiveRecord. But it will be easier to come back to them, get rid of AR (or even Models entirely?) and use just the adapter as we’re not accessing DB in any other place. It’s never too late to learn and try new things!

Use cases

As for use cases - those are our layer of abstraction between data access and representation. A clear description of user’s tasks.

Benefits? Thin models and controllers. Code organized in a way when someone would look at our use_case catalog he could probably describe wireframe of our business logic.

# use case
module UseCase
  module Email
    class Update
      def initialize(params, callbacks)
        # initialize params

      def call
        return callbacks[:success].call if email_updated?
      # other needed methods

# controller
def update
  success = -> { redirect_to emails_path, notice: “Yay” }
  error = ->(error_message) { redirect_to emails_path, alert: error_message }, success: success, error: error).call

Again, testing is easier as in controllers' specs we only check if UseCase got called. In use cases' specs, we only check if repos got called and... the rest you know. Fast and clean!

Terraform and Ansible

I had little experience with performing the first release. In the past, I had been either working on an existing projects or with technical clients who would take care of it themselves. Here—no such case. As the project was going pretty fast, we decided to host staging and preprod environments on Heroku. Production was supposed to reside on AWS.

So I went: into the AWS console, created a domain on Route53, new EC2 instance. Logged in through SSH and set up the environment using some step by step guides from the web. Fired up Capistrano docs (as this was the solution I knew from past projects) and started writing proper scripts to deploy the master branch from GitHub. Aaand... it worked!

What I didn’t know then, the setup was so prone to failures… it failed.

There was no monitoring, no automatic services restarts, everything was set up on a single instance so no horizontal scaling.

I reached out to our ops guys and they introduced me to a brave new world of infrastructure as code. I won’t delve into the topic as Jan already discussed Terraform remote backend problem, but three good things came out of this:

  • We wrote our first post-mortem document - what happened, why, when, how we fixed this and how to prevent it in the future. This not only helps us avoid similar mistakes in the future but also shows the client that we are professionals. We can admit the mistake and are competent to recognize it and quickly fix it.
  • I started reading The DevOps Handbook - a title that changed my state of mind about how to approach the project after several first pages (reading still in progress).
  • We had (and still have) a stable production working (and preprod with the same infrastructure)!

Act Three—Being A Newbie Tech Lead

You know how all that god-forbid-long motivational posts can be summed up with just do it? Well, I’m not gonna do that to you. You see

I never asked for this.
— Adam Jensen

But it was so worth it.

In my career as a web developer, I was mainly orbiting around my own pull requests. Sure, I was active in meetings, making suggestions, and code reviews. I always cared about the projects I was involved with doing a good job. But bigger responsibility has never laid on my shoulders. I was comfortable where I was.

Until this one. When Magda was pulled out to another project, I naturally became a tech-lead. I could decide what technologies we’re gonna use, choose libraries and patterns. I played it safe on some fields (setup) but also had a chance to try new things (mainly with software architecture). I did make some mistakes and was stressed sometimes. I know I WILL make new mistakes but what I learned is mine (and now yours too). 🎉

Some milestones I understood (or was reminded of):

  • Fast decisions and shortcuts can speed up the MVP, but they also create technical debt that WE MUST TAKE CARE OF LATER. Which we, fortunately, did thanks to transparency and complete honesty with the clients. They wanted some things faster, we agreed but pointed out that we will have to go back to them. Everyone was on the same page the entire time.
  • The first deployment failed but we made a small revolution and raised people’s awareness of how the company wants to standardize things ops-wise (Terraform + Ansible). I learned a thing or two about AWS console and even wrote some architecture as code scripts.
  • We started to use planning poker to estimate effort, not time. We divided tasks in a way we could deploy something for users almost every day even if it wasn’t perfect. Iteration rules!
  • ALWAYS be prepared to specification’s change - what we think, what clients think and what users think is sometimes three completely different points of view (iteration strikes again!).
  • I was not riding a bicycle this time. We were steering a ship. I had to think not only about my tasks but keep the team satisfied with the project and features they were working on (from the more technical perspective and side-by-side with our project manager of course). We were brainstorming and pair-programming. Everything to keep everyone up-to-date with the features and business logic. It’s always easier to come up with a solution while discussing options. For example, how to optimize uploading and calculations performed on huge spreadsheets from users. If any of us would go with the initial idea, the results would come up only just on code review. Too late to implement it differently and rewrite the code.
  • Communication is crucial. Weekly calls with updates build trust. I think what I would want to say is best described by our client:

Quote from a client

Act Four—See You On The Flip Side

Two days after project freezing to stabilize, increase the user base and find investors I was suggested to write about it. About what I learned from the experience. The post, I think, came out a little chaotic (as the whole adventure was!) but that’s actually another thing I’m learning here—to write such blog posts.

Personally, the project was a combination of “first times” for me.

First time writing an audit.
First time performing the whole-project estimation.
First time choosing all technologies.
First time leading a project (a lot of prioritizing, compromises, creating technical debt, reducing technical debt).
First time applying new patterns from the ground up.
The first release of a whole new app.
First major failure (and many minors).
First post-mortem.
First one-to-one calls with a non-tech client.

This chapter ends here. The things I learned are invaluable but the most important is the fact that the app is live and gaining users every day. The fact that client used word “encouraging” so many times when going through the MVP it made us fly. The fact that we succeeded 🎉.

Want to build meaningful software?

Pay us a visit and see yourself that our devs are so communicative and diligent you’ll feel they are your in-house team. Work with experts who will push hard to understand your business and meet certain deadlines.

Talk to our team and confidently build your next big thing.

Mateusz Karbowiak avatar
Mateusz Karbowiak