Fine-Tuning BERT for multiclass categorisation with Amazon SageMaker

Posted by on September 15, 2021

Intro and Context

When FreeAgent customers import their bank transactions, we predict which accounting categories the transactions belong to by making requests to a machine learning model managed with Amazon SageMaker. The model inputs are bank transaction descriptions (e.g. ‘Costa Coffee Edinburgh’) vectorized by using a tf-idf bag-of-words approach1, and the model itself is a linear support vector machine (e.g. chapter 12 of the elements of statistical learning2).

This approach is simple both in terms of the preprocessing and modelling, and has been running on the smallest real-time inference instance SageMaker has to offer, so costs have been low! However we discovered a scalability issue that has been hampering us for some time. This issue is that the vocabulary learned when vectorizing the training data transactions, which considers both unigrams and bigrams, can reach the 10s of millions. The number of parameters we have to store is pretty much equivalent to our vocabulary size multiplied by the number of target classes when we train the model with a one vs. rest strategy. Adding more target classes to the model therefore increases the number of large matrix multiplications required to serve the predictions, affecting model latency, and also the space required to hold the model object in memory.

For this reason, as well as curiosity as to whether vector representations based on an attention mechanism3 will perform better than bag-of-words, we were keen to explore using a BERT4 model for transaction categorisation. In this post we discuss how we made use of the Hugging Face transformers library5 to fine-tune a BERT model to categorise our bank transactions. 

The work described was carried out together with our summer intern, Harry Tullett.

Fine-Tuning BERT for multiclass problems

BERT is an approach for constructing vector representations of input natural language data based on the transformer architecture6. The representations are learned by pre-training on very large text corpora and can be used as inputs when learning to perform various downstream tasks – a process referred to as fine-tuning. BERT has been instrumental in the adoption of transfer-learning for natural language processing in the same way as ImageNet7 for computer vision. The family of transformer-based models8 achieve the current state-of-the-art performance9 in tasks such as machine translation, named-entity recognition, question answering and sentiment analysis.  

Since we already use SageMaker and other AWS services like S3 and SageMaker Studio we wanted to stay within this ecosystem and make use of the fantastic set of pre-trained models and training APIs provided by Hugging Face. To get started quickly, we followed an example notebook that uses the Hugging Face framework in the Python SageMaker sdk to fine-tune a binary categorisation model. 

What we struggled to find was an example showing how to fine-tune for multiclass categorisation with a custom dataset. This post contributes a description of how to modify the above example to train multiclass categorisation models in SageMaker using CSV data stored in S3.

Our setup

We use SageMaker studio with a Python 3 (PyTorch 1.6 Python 3.6 CPU Optimized) kernel to run our code, and install the required packages on the first line as follows:

!pip install 'sagemaker>=2.48.0' 'transformers==4.9.2' 'datasets[s3]==1.11.0' --upgrade

Configure estimator source and output

The source_dir and output_path attributes of the Hugging Face Estimator define paths to the source directory of the training script (as a tar.gz file) and model outputs respectively. These should be set to appropriate S3 prefixes.

    'epochs': 1,
    'train_batch_size': 32,
    'model_name': 'distilbert-base-uncased',
huggingface_estimator = HuggingFace(
    hyperparameters = hyperparameters,

We first tested this by using the tokenized imdb data saved to our own S3 buckets as inputs and calling fit on the estimator to check if our SageMaker studio role had the required permissions to interact with S3 and SageMaker. The SageMaker training job successfully completed and model outputs were written to the expected S3 location. 

Read custom data from S3

Satisfied our permissions were set correctly, we started tackling the multiclass problem. Our training and validation data are stored in CSV files in S3. We load these into pandas dataframes with the read_csv method, which uses the s3fs package to read directly from S3:

train_df = pd.read_csv('s3://bucket/prefix/data/training/train.csv')
val_df = pd.read_csv('s3://bucket/prefix/data/validation/val.csv')

Encode target variable and format headers 

Each row of these dataframes contains the text description of a bank transaction in a ‘text’ column and the assigned accounting category (e.g. INSURANCE) in a ‘category’ column.

This categorical target is encoded as an integer and the name of the column is changed to ‘labels’ as expected by the HuggingFace model. The following examples shown for the training dataset are also applied to the validation data:

from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
train_df['category'] = le.fit_transform(train_df['category'])
train_df.rename(columns={'category': 'labels'}, inplace=True)

Create Hugging Face Dataset objects

As per the original example notebook we load the data into a Hugging Face Dataset object, but do this from memory using the from_pandas method:

from datasets import Dataset
train_dataset = Dataset.from_pandas(train_df)

Tokenize and pad the input data

The text column is tokenized and padded before passing to the fit method of the Estimator. This is the process of applying the pre-trained tokenizer associated with our chosen BERT model to the bank transaction text to generate input ids and an attention mask.

We tokenize batches of data and pad these to a common length (i.e. a common number of tokens) because each transaction in a batch passed to the model must have the same length. The input ids are used to lookup the vector representations of the corresponding tokens in the pre-trained BERT model.

The fine-tuning process involves passing vectors representing the token sequences to a feed-forward neural network head attached to the BERT architecture, which outputs probabilities for each of the target classes. Backpropagation occurs through this entire architecture in order to compute the updates to the weights and biases that will minimise the cost function for our classification problem. 

The code for the tokenization is as follows:

from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained('distilbert-base-uncased')

def tokenize(batch):
    return tokenizer(batch['text'], padding='longest', truncation=True)

train_dataset_tokenized =
    columns=['input_ids', 'attention_mask', 'labels']

Note we’ve updated the tokenize helper method defined in the notebook to use padding='longest', which pads all the transactions in the batch to the size of the transaction with the most tokens.  We find this is typically ~50 tokens in our use case. When left as padding='max_length', every transaction is padded to a size of 512, which is the maximum length that can be passed to the BERT model.

This turns out to be really significant in terms of training times, GPU memory utilization during training and the size of the output model artifacts. An even better strategy would be to batch similar sized transactions together, as most of our examples have much fewer than 50 tokens. We pass the tokenize method batches of 100 transactions and have to be careful to train the model with batches that are a multiple of this to ensure everything has the same length.

At this point the format of the dataset is set to torch so that Pytorch tensors are returned, and the unused ‘text’ column is dropped. 

Final step: save formatted padded data

After these transformations we’re almost there! The datasets are saved to s3 as follows: 

from datasets.filesystems import S3FileSystem
s3 = S3FileSystem()  

training_input_path = 's3://bucket/prefix/data/training/train_tokenized.csv'
train_dataset_tokenized.save_to_disk(training_input_path, fs=s3)

Modify the training script for multiclass categorisation

A couple of very minor changes need to be made to the ‘’ training script defined in the notebook to facilitate multiclass categorisation. These are to add the num_labels attribute to the model definition (the number can be found from the label encoder with e.g. len(le.classes_), in our case 15):

model = AutoModelForSequenceClassification.from_pretrained(

and to change the average type in the compute_metrics method from binary to micro (or one of the other multiclass options):

def compute_metrics(pred):
    labels = pred.label_ids
    preds = pred.predictions.argmax(-1)
    precision, recall, f1, _ = precision_recall_fscore_support(
    acc = accuracy_score(labels, preds)
    return {
        'accuracy': acc,
        'f1': f1,
        'precision': precision,
        'recall': recall

This file is then tarred as ‘train_script.tar.gz’ and uploaded to the s3 location defined in the Estimator source_dir attribute.

Finally we change the train_batch_size in the hyperparameters to 100 to match the tokenizer batches as discussed above, define our Estimator and call fit:
    {'train': training_input_path, 'test': val_input_path}


The logs are streamed to the notebook when running from SageMaker studio so it’s easy to see the progress of the training job. When the job completes you’ll see the model outputs in the specified s3 location.

If you’re only interested in the final model for making predictions, you can add save_strategy='no' to the TrainingArguments in the train script, which will stop checkpoints being saved and greatly reduce the size of the model outputs.

Use the model artifacts for inference

This model can be deployed to a SageMaker real-time inference endpoint following the steps in the notebook. 

However, if you just want to experiment offline by downloading the model outputs to make predictions you can use a Hugging Face pipeline to output confidence scores: 

from transformers import AutoModelForSequenceClassification
from transformers import TextClassificationPipeline
from transformers import AutoTokenizer

model = AutoModelForSequenceClassification.from_pretrained(
tokenizer = AutoTokenizer.from_pretrained(
pipe = TextClassificationPipeline(
pipe(['Costa Coffee Edinburgh', 'TFL Travel London'])


Very preliminary evaluation suggests that the accuracy of predictions from our fine-tuned BERT model, completely out of the box, is slightly higher than predictions from the linear SVM. This, combined with the scalability advantages described in the introduction provides good motivation for further investigation into this modelling approach.   

Hopefully this post saves a bit of searching through documentation for someone! We’d be keen to hear from anyone working in the area of transaction categorisation, particularly which preprocessing and modelling approaches you’ve been working with, so please do get in touch if this was of interest!


  1. scikit-learn text-feature extraction:
  2. Elements of statistical learning ii Chapter 12:
  3. Attention distil blog post:
  4. BERT paper: Devlin et al. 2018:
  5. Hugging Face Transformers:
  6. Transformer Architecture: Vaswani et al. 2017
  7. ImageNet website:
  8. Must-read papers on pre-trained language models:
  9. Current state-of-the-art in machine learning tasks:
  10. Python sagemaker sdk documentation for the Hugging Face framework:
  11. YouTube video on fine-tuning a Hugging Face transformer model using SageMaker:
  12. Getting Started notebook to accompany above YouTube video:

Software engineering: 5 things they don’t teach you at university

Posted by on September 10, 2021


Over the past couple of months, I’ve been working as an intern software engineer in the Banking Integrations team here at FreeAgent. Before I started, I had just finished the second year of my Computer Science degree at the University of Birmingham. 

Whilst university taught me a multitude of skills, there are many that can only be truly gained from working in a company. I’d like to share some of these below.

1. Navigating a large codebase

University taught me how to solve problems using code — a very useful skill. However, I now realise that being a good programmer does not necessarily make you a good software engineer; there are other equally important skills like teamwork, maintainability, and understanding code written by other people. 

Furthermore, at university you often write small programs on your own to accomplish a specific assignment. It is often not important that this code is unit-tested, follows good practices, and is properly commented. The importance of these things became self-evident when navigating the FreeAgent codebase, which had been worked on by — at the time of writing — 163 developers. I also struggled to navigate the codebase at first, simply because I’d never worked on programs with more than a few files before.

Tip for incoming interns

Make use of your IDE’s universal search functionality for files, keywords, and method names. Also, look into ripgrep.

2. Working with APIs

One valuable skill that wasn’t covered during my time at university is working with APIs. Before coming to FreeAgent, I had no idea how to make an HTTP request. During the first couple of weeks, my wonderful teammate Will helped me set up Postman so that I could query FreeAgent’s public API. At first, I didn’t know how Postman was making the requests — it seemed like magic. Later on, I learnt how to make requests manually (i.e. on the command line): first by using curl, followed by the more user-friendly httpie. This way, I managed to understand how the requests were formed by Postman under its complex graphical interface.

Tip for incoming interns

Practise with a public API (e.g. the Spotify Web API) first. Try to familiarise yourself with JSON, using access and refresh tokens, and the different HTTP request methods you can use to fetch data from the API. 

3. Teamwork

At university, you write most of your code by yourself. University didn’t prepare me for daily stand-ups, retros, code reviews, and making my own pull requests. In fact, my university only taught me a few basic git commands; I had to pick up most of the ones I now know during this internship. 

One key difference between university and a software company is that whilst university assesses your work individually most of the time, in a software company everything is done in a team; you have to learn to be a good team player. 

Tip for incoming interns

I would recommend looking through the pull requests of an open source project on GitHub (e.g. Jekyll, but there are loads). You could even try cloning it and making some changes yourself, followed by your own pull request.

4. Unit Testing

Another skill that university didn’t prepare me with is writing well-structured, clean and unit-tested code. At university, you learn the syntax and various quirks of a particular language, but you never learn its best practices. We were only taught about unit testing in the context of passing pre-written tests for each section of an assignment.

Now I understand that writing unit tests is often more important than writing the actual code itself. Good unit testing makes it easier to prevent and identify bugs, leading to a more maintainable codebase. 

Tip for incoming interns

If you aren’t used to writing your own unit tests, here’s a useful tutorial on how to do so using RSpec.

5. Web Frameworks

Prior to this internship, I had little experience working with web frameworks. Most of the coding assignments at university are focussed on specific computer science concepts. To build a big web application like FreeAgent, however, you need a web framework (e.g. Ruby on Rails). 

I had used the Django web framework in one of my personal projects and this made it easier to learn Rails, since a lot of the fundamental concepts (like MVC) are the same.  

Tip for incoming interns

I would recommend going through the “Getting Started with Rails” tutorial. This will save you a lot of time when it comes to familiarising yourself with the FreeAgent codebase. 


My degree in computer science has taught me many useful things: data structures, algorithms, programming paradigms, networks, databases and the basics of web development. These form the foundation of a software engineer’s toolkit. 

However, this internship has allowed me to see that many skills and experiences are not — and sometimes cannot — be gained at university. These include navigating large codebases, working in a team and, most importantly, getting to see my code live in production being used by real people. Should you find yourself in a position to apply for an internship at FreeAgent, I’d highly recommend it.

I hope you enjoyed reading this post and found it useful. If you’ve recently completed an internship, feel free to get in touch — I’d love to hear how your experience compares.

The Value of an Internship at FreeAgent

Posted by on September 9, 2021

Coming from a chemistry background, I’d never have thought that the skills I was learning at university could be transferred to working as an analytics intern at FreeAgent, but this summer I proved myself wrong. After spending a lot of time taking courses in Python and SQL with the Code First: Girls charity through a year of lockdown, this internship was the perfect opportunity for me to develop everything I had learned even further. Over the course of the summer, I was tasked with working on a project designed to calculate the ‘lifetime value’ of FreeAgent’s customers. In doing so, I have found a new level of confidence in my own skills that I am sure will stay with me wherever I go next.

What is Lifetime Value, and why is it so useful?

FreeAgent is a subscription-based service which generates recurring revenue through subscription payments from different groups of customers. Estimating the expected present value of expected future payments, or lifetime value (LTV) of different customer segments can help FreeAgent make better-informed marketing decisions. My aim this summer was to create a reliable process for calculating LTV.


Calculating an estimated LTV is a huge undertaking; one that can take a very long time depending on how thorough the results need to be. I only had three months to complete this project at FreeAgent, so prioritisation was key. I needed to understand the company’s customer segments and how they are tracked and reported. I also needed to know which of these metrics are considered to be the most important by the people who use the data. This ‘discovery’ stage proved to be as big a learning curve as any other part of the process.

Initially, a lot of my time was dedicated to modelling many different methods of predicting how long a customer might use FreeAgent for and then calculating estimated LTV. I was keen to use the programming language ‘R’ to do this but I had never used it before and had expected to need a significant amount of help to get up to speed with it. However, with my team giving me the freedom to work at my own pace while supporting me when I needed it, it turned out that I was able to work out nearly everything on my own! While this approach might have taken longer than if the team had simply shown me what to do, it made the overall process so much more rewarding. In the end, it also saved time as it left me better equipped to deal with any problems that arose. 

Using R for the modelling exercise gave me the ability to quickly test any theory I could think of and collect the results for easy comparison. This approach allowed me to really understand the advantages and disadvantages of the various methods I had available to me and to feel confident about the decisions I made.


While developing our approach to calculating an estimated LTV using R had many advantages, it wasn’t a perfect solution. The required data had to be extracted by running different SQL queries by hand. While it’s possible to automate R scripts to run the queries, this isn’t a standard method used at FreeAgent and it could have introduced additional maintenance requirements.

As a result, a decision was made to combine the SQL and R logic using FreeAgent’s ETL tool, Matillion. Having never heard of the tool before, I found this to be one of the most unanticipated parts of my internship, but learning to use Matillion was a rewarding process. Rather than working with data in isolated chunks, I was able to gain a fuller understanding of the ways in which data is collected, transformed and maintained. It also forced me to take the time to truly understand every step of the maths I was using, rather than hiding behind the easy-to-use functions that R is so useful for. While it was unexpected, learning to use Matillion was one of the experiences that made my time with FreeAgent so worthwhile. 


I’m very grateful to have had the opportunity to work end-to-end on a project like this. From planning and developing the initial solution, to re-implementing the method in Matillion and then presenting my solutions to stakeholders, I have had a full, comprehensive experience of life in the analytics team at FreeAgent. The internship has taught me far more than simply technical skills and the advice and support I have received from the team has been invaluable. If you’re considering applying for an internship at FreeAgent, here is your sign to go for it!

New wine, old skins – how FreeAgent blends existing tools and fresh approaches

Posted by on September 8, 2021

According to the parable in Matthew’s gospel, “no-one pours new wine into old wineskins – otherwise, the wine would burst the wine-skins, and both would be ruined.” So, coming into FreeAgent as an intern, I wondered – would FreeAgent prefer the new wine in the new skins or the old wine in the old skins? Would they satisfy the stereotype of the tech start-up, move fast and break things, trip over themselves in the rush to adopt the latest technology? Or would they satisfy the stereotype of the banking group subsidiary, play it safe, allow their ancient systems to gather dust? As I discovered, FreeAgent doesn’t satisfy either stereotype. In fact, FreeAgent blends old and new, balancing the advantages and disadvantages of each. And preferably without ruining any wine or wine-skins.

So how do you balance old against new? Well, there’s a pretty obvious advantage to using new tools – sometimes, the newest tools are the best tools. After all, a new tool would never have been invented if there hadn’t been somebody, somewhere, who thought it would be better than whatever came before. Second, tech is a fast-moving industry – if you don’t keep up, you could be left behind. That means less testing, a smaller community to help you when you get stuck, fewer security updates and a smaller talent pool to hire from. And, of course, from time to time somebody scores a direct hit with a shooting star and invents something totally new, something beyond a mere efficiency improvement, something that opens up whole new possibilities.

But that doesn’t mean it’s all on the side of the newcomers. For a start, adopting new tech usually has a cost – you might need to re-train to use the new tech effectively, or repeat a load of old work. You can avoid all that by simply sticking with what you’ve got. Furthermore, if you use the same tool for a long time, you can build up your expertise – FreeAgent, for example, has a lot of industry-leading talent in the Ruby programming language, because they’ve been working with it for a long time and nurturing their team’s skills. Lastly, if something’s old, that means it has a track record. You can check that it lives up to its promises. With the newest tools, you might not have that luxury: adopting something before it’s really proven in the field is a gamble.

What does this all mean in practice? Quite a lot, actually! There are any number of examples I could point to where FreeAgent is currently managing this balancing act. I’ve picked a comparison I thought particularly illuminating, comparing a case where we stuck with the old against a case where we adopted something new.

At FreeAgent, I learned to work using the Lean-Agile method. Lean-Agile is a way of organising work, and is a fusion of multiple pre-existing methods. The Lean method was invented in Toyota in the 1950s, and is now standard across industries as diverse as manufacturing, healthcare and software development. Agile, on the other hand, is mainly used in software engineering. Although the term “agile” was only coined in 2001, similar methods had been in use in the industry for almost half a century previously. In the mix go a few other long-established software-engineering paradigms, and out comes Lean-Agile. The method prioritises making as short as possible the loop from identifying business requirements to making functional software to testing the results. Lean-Agile is now ubiquitous in software engineering.

So it’s not surprising that FreeAgent uses Lean-Agile. But why? Aren’t there other approaches? (There are – lots!) But using Lean-Agile is a clear win for sticking with what you know. It’s so ubiquitous that a new engineer can pretty much jump straight in and already understand how to work effectively. I saw this myself when a new engineer joined the team at the same time as me – within a week, not only were they fluent in the system, they were helping to improve it! Best of all, it just works. And we know that because the methodology is about as old as software engineering itself. Lean-Agile didn’t become standard by accident. Companies tried it, it worked, and so it spread. The risk and cost of trying something left-of-field just isn’t worth it.

In contrast, while I was there, FreeAgent tried something radically new. For 12 weeks, nobody at the company worked more than four days a week, although we were all guaranteed the same weekly pay. This experiment clearly cost the company a lot of money, and wasn’t without risk – fewer workers online at any one time meant fewer people available to respond to an emergency.

Nonetheless, FreeAgent decided it was a good trade-off. Why? For a start, some of the risks of new ideas were relatively manageable. Other companies, especially in tech, have tried four-day working weeks before, and some said it worked well for them. If it worked well for them, that makes us more confident it could work for us, too. Plus, there’s good evidence that working four days a week makes you significantly more productive. Finally, in a tight labour market for software engineers, offering four-day contracts at a competitive wage could make FreeAgent more attractive to potential recruits. So, in line with FreeAgent’s modus operandi, we tried it out. We’ve measured the results. In time, we’ll get the verdict on how well it worked. And if it worked for us, you can be sure that we’ll be open to four-day contracts in the future.

That’s just one of many examples I saw at FreeAgent where we managed the trade-offs between old and new. Thanks to my time at the company this summer, I’m now better at judging when to prefer the old wine, when to prefer the new skins, and how not to ruin either.

I Came, I Saw, I Categorised

Posted by on September 7, 2021

It’s been a wild time this summer at FreeAgent: four-day weeks, Edinburgh flooding, people gearing up to return to the office, and in the meantime you have me, the remote software engineering intern, working away in his little corner of the FreeAgent world. But before all that, who is this wee guy? Well, I’m Fraser Dempster and I spent 12 weeks of my summer at FreeAgent.

Excuse me, I’m lost (virtually)

It didn’t take long for me to mess up. On the first day I was late to the induction with the other new starters (of which there were nine). I hadn’t managed to find the email to join the first ever call, so in one sense you could say I was lost, albeit virtually. I had already missed a major part of my first day – great. But after that it was go go go – from creating accounts to setting up my workspace and completing onboarding tasks, there was a lot to do on that first day. Having been to university, I was used to being spoon-fed my admin work, so the change in pace was drastic.

Meet the mobile team

After that first day, it was time to meet the mobile team, which had always been fully remote – veterans of the working from home culture. Aside from their remote experience, I felt spoiled to witness how adept the team were in their respective fields, evidenced by the jargon exchanged in that first call. The team dynamic relied on variety – from product manager and designer, to iOS and Android software engineers, there was a lot more involved in a software development team than I imagined, and I was impressed. 

So I wondered how things really worked within the mobile team. I looked for more information regarding the mobile app, which led me to the product manager (Roz). She kindly shared the “mobile roadmap”, whereby, firstly, features are proposed based on analytics and user requests. Then, these proposals are studied and better understood through Figma (a collaborative prototyping tool) designs, time estimations, and value to users, and if all goes smoothly, the properties of a mobile feature are assimilated and eventually developed by the software engineers. The process is perfectly streamlined in order to prevent inefficiency within the team and to maximise output of impactful features. One of which I was about to undertake.

Spending category report – ehm, what?

My summer project. What’s it all about? Making use of user data, many new banking models incorporate categorised spending reports to aid users in accounting for their spending. I personally valued this feature, as did users, and that is what I would be working on this summer. To prevent any frivolous attempts at further describing the feature, the Figma design created by our amazing designer (Barry) is attached below.

The process to achieve that design begins with an Application Programming Interface (API) to input all the data displayed on the screen. An API is like the waiter you tell your order to – it collects your order, informs the chef, then consequently serves up your meal. When the API was ready, the mobile development would begin. This was split into two phases – the first one for creating the list, titles, and URL button from the design above – everything excluding the graph. Hence, the second phase would be solely focused on developing the graph, due to its complexity. The initial design, for example, required horizontal scrolling and an accompanying gesture hint, but as time went on, requirements changed and the design reflected that, as I explain later on.

API building

Building an API was an entirely new experience for me, as was Ruby on Rails, so to get started I was recommended to do the Ruby on Rails blog tutorial – which I highly recommend for any aspiring Rails developers. After that, I wrote an API specification which detailed a summary of how the API would function, the request URL, and the response schema. The response schema detailed an example of what the returned JSON object should look like. Shortly, the API will be made available to our API users. After having the specification reviewed by the team, and addressing any questions/concerns, it was time to begin creating the API on Rails.

The process of doing that involved understanding the Rails architecture. Namely, the Model View Controller (MVC). The MVC separates the application into three parts. The model is for handling data and business logic, the controller is for handling the user interface and application, and the view is for handling the graphical presentation (in my case the JSON object).

To tie that into what I was doing, I used the controller to grab the previous six months from the present date because whenever a user opened the feature, we would show six months by default. The model was used to do the calculations and logic so that the data was ready to be displayed. For example, mapping the months to the correct time period of the report, or getting the appropriate title for an item using the current company and nominal code. Lastly, the view was used to format the returned JSON object in an intuitive way, preparing it for adoption in the mobile app.

In the meantime…

Aside from developing the spending category report, I had the opportunity to discuss my project at the Engineering Forum. The Engineering Forum is a weekly meeting for engineers to share a specific technical topic, something they learnt that week, or for interns to showcase what they’ve been up to – that’s where I found myself. It was a great opportunity to be able to practice public speaking over Zoom and I learnt the hard lesson of not knowing my jokes’ comedic effect as there was minimal visual feedback from the audience and mics were muted. Alas, it was really fantastic to share with everyone how to create an API endpoint for the mobile app and it really helped me consolidate my knowledge as well. On top of that, the feedback was rewarding and I enjoyed answering a few questions on the topic too.

Android development (Part 1)

The FreeAgent app is native to both iOS and Android. After discussing it with my manager, we decided it would be best to develop the Android app as I have a Samsung phone. The app was written in Kotlin, which was unfamiliar to me, but I was assigned one of the senior Android developers (Moh) to buddy with me, which helped immensely. Setting up the app was simple: download Android studio, clone the repository, and plug in my phone (to use as the emulator for the app) and it was ready.

Having experienced learning a new architecture for Rails previously, I felt more equipped this time to tackle the Android codebase and architecture. The native Android app uses the Model View Presenter (MVP) architecture. This differed from the Rails MVC in that the presenter dealt with all the business logic, receiving the desired data and formatting it for the view. The presenter also communicates with the view through an interface which allows for mocking of unit tests. The model defines the data to be displayed and the view handles where the data should be displayed.

Having that understanding down, the next step was to dismantle each part of the design. At a glance there were two text boxes holding the month/year and total spending for that month, a list of the categories with their corresponding spending, and a URL button. Having the API ready to go, the mobile app needed to be able to decipher it into something it could understand. To do this, I created a ‘repository’ in the codebase that would grab the API data and translate it into an object the app could understand. Once that was done, the display needed to be created to look as similar as possible to the design. 

The trickiest part was the list of categories in the design. Since the list in the design had to be dynamic (always changing in size) it could be anything from one item in size to 2,000. This means there is the potential pitfall that if you try loading a huge list, the app could slow right down, and that isn’t what users want. Solution: RecyclerView. A RecyclerView only loads items as it needs them, meaning that a massive list would never load all at once. Bingo!

The extra buttons in the screenshot above are to allow navigation of the months, since the graph didn’t exist.

A lesson learnt

When the spending category report matched the design (aside from the graph) I thought I was done with part one of Android development. How wrong I was. After boasting to the team of my success, the real work began. The mobile team tester (Jess) got down to business. And after testing my feature, there were a boatload of bugs that needed to be addressed. So I got back on the grind. And this process repeated itself until most bugs were ironed out. But then I had to create a Pull Request (PR), write up Pre-production testing (PPT) and also fix the tests I had previously written, which all took time – around an extra two weeks. 

Finally, after part one was done, I had the pleasure of reflecting on my experience in the team’s retrospective where I wrote in the “went badly” column that “the spending category report was delayed”. In response, Joe, an Android developer, told me that “the last 10% of the work takes 90% of the time” – a rule that will stick by me throughout my career as a software engineer. Thank you, Joe.

Android development (Part 2)

Having spoken to the mobile designer and other Android developers, the design of the graph had to be changed. It was decided that instead of a horizontal scroll, the graph would use two buttons to navigate between six-month periods, as this eliminates any uncertainty in utilising the feature from the perspective of a user. Everything else was to be kept the same. 

The toughest part was drawing the graph, as this had to be done manually on Android using Canvas. There were similar examples within the app already that I leaned on for support, and I was assigned a new buddy (Joe), who always helped me clear any blockers I had. To do the positioning, I had to create a ratio that each bar and bar gap would abide by so that the graph would be drawn equally on the x-axis. To draw the height, I used a simple formula that would divide the absolute value (amount) of each bar by the maximum value between all six bars and then multiply that by the height of the canvas area (where the graph could be drawn). The last thing was making the bars clickable. To achieve this, each time a user lifted their finger off the screen I looped through a list of bars that had the same x-coordinates of each bar and the max height of the canvas and detected whether there was a specific bar within that column. If there was, the bar would be highlighted and the data displayed would change.

Remote working

The elephant in the room, specifically my room, is remote working. This hasn’t been the conventional in-office internship and that changed a lot of things. There was no ‘watercooler chat’, lunch with colleagues, and my job commute was precisely three seconds from my bed (I haven’t been late to work yet). At the beginning, it was tough working remotely because a lot of problems were amplified due to the uncertainty of being a newcomer. FreeAgent, my team, and the culture really supported me in navigating remote work, and here’s how.

Frequent one-to-ones with my manager (Anup). These helped give me a sense of direction while working. Whether it was with the project, presentations or just to satisfy that social itch, these meetings were always welcome and encouraged me to approach fellow programmers and other mobile team members.

The culture of helpfulness. There has never been a time throughout the internship that I felt isolated and unable to ask for help, thanks to the team and the culture cultivated here at FreeAgent. However simple the question was, someone wanted to answer it, and that made me feel comfortable at my virtual workplace. 

Town hall. Prior to starting my internship, I was intrigued by the town hall concept, but I know much more about them now that I’ve sat through a number of virtual town hall meetings. The cooperative culture is evident and inspired me at the end of the week. It also helped me stay curious and apprised about other teams’ events.

Keeping a diary. From the beginning, I kept a daily diary to track what I had been up to throughout the internship using Notion. Diligently, but also in a relaxed manner, I documented my highlights. This helped consolidate what had happened during each work day and gave me time to reflect on how it could have gone better or worse.

Meeting the other interns. Having weekly meetings with the other summer interns was a good way to loosen up and share what each of us had been up to during the week. The environment promoted was relaxed and everyone was in a similar situation where they were learning new things.

Concluding my summer internship

It’s now time to trade my FreeAgent summer for an academic winter. No more town halls and virtual coffee chats. I’ve also met and said goodbye to fellow colleagues who left during my internship, to whom I wish well. Everybody I have met has shaped my knowledge and understanding of software engineering one way or another, and that will undoubtedly influence my career. I’ve tackled challenges and expanded my skills. Thank you, FreeAgent.

Lessons from my first software engineering internship

Posted by on September 6, 2021

I started university two years ago with minimal programming knowledge and only a few lessons making Pong in Scratch under my belt. So in the beginning of my Computer Science degree, I spent a lot of time playing catch-up. My experience here at FreeAgent is what has finally caught me up.

Unlike in most internships, it was business as usual from day one for me, rather than one big project. This gave me exposure to a great amount of the codebase, and the work I did varied vastly from week to week.

I began my internship working on the front end of FreeAgent, improving the onboarding process of new clients for our accountancy practice partners via their dashboard. This was the only project I oversaw from beginning to end, an opportunity I was very grateful to experience.

The second half of my internship was spent removing mentions of accountancy services whose white-label had been removed from the codebase. This is where I achieved my greatest accomplishment: during my internship I officially deleted more lines of code than I added! Don’t be mistaken, though: it was not a simple job of pressing backspace like I initially thought. Instead, many hours were spent making sure the lines deleted did not affect any other areas of the codebase.

I was also given the opportunity to be on ‘bug catcher’ duty (a term used here at FreeAgent for an engineer whose role is to fix any bugs found) where I had to put my detective hat on to recreate steps to figure out how bugs had arisen. 

So why not share some of the lessons I have learnt, as someone who only entered the software engineering world two years ago?


The simplicity of Ruby promotes quick development and easy collaboration. It is a language that, even with minimal experience, is quite easy to read. This means less time spent on understanding convoluted syntax and more on understanding what the code is trying to achieve. There is a lot of magic that goes on in Ruby on Rails, a term I have heard quite a bit here at FreeAgent. It took me a few weeks to realise that this is a term that a lot of Ruby programmers, both in and outside of FreeAgent, use to describe the simple directives that execute complex operations, rather than a general descriptive phrase. 

As is common with a lot of Computer Science students, my course mainly used Java. Ruby, in comparison, is more streamlined as it takes less code to write basic structures. On the other hand, Java is generally faster and more efficient. The key difference between a statically-typed language and a dynamic one is when errors are detected. With Java, I tended to catch errors earlier in development, as everything needs to be meticulously labelled but, in my experience, this slowed down the development process. The pros and cons for me were equally weighted in both languages. By learning Ruby I now have the choice of two very different and powerful languages.


When I first started programming personal projects, I either stored all the files locally on my machine or spammed hundreds of commits straight to master, as keeping the codebase organised wasn’t a skill I had quite mastered yet. Cut to now, where I use git multiple times every day. But when I first started my internship at FreeAgent, the first commit, the first push and, worst of all, the first pull request were all frightening. Never had I had felt such a responsibility with the code I had written; in my mind it was as though with one tiny mistake I could bring the whole site down! Fortunately, such power does not lie in the hands of engineers here; instead, there is a meticulous process in place that ensures all is well before your code is merged with master. Firstly, it is vital that your code passes all the applicable tests in the codebase in Jenkins. It then needs to be code reviewed for any improvements to make or small mistakes to correct. Finally, right before merging, your code needs to pass pre-production testing (PPT) to ensure it hasn’t broken any part of FreeAgent. 

The other important tool is one that is integral to any operating system -the terminal. From watching the engineers on my team during our pairing sessions, I have noticed that implementing the habit of using the terminal for almost everything is crucial to be a successful software engineer. The common factor that I saw through my time pairing with the engineers on my team is that efficiency is key. Initially learning my shortcuts and customising the terminal did not seem too important but the speed of the development was greatly increased once implemented.

University vs industry code

What I was most curious about before starting my internship is what codebases look like for big applications. So I was very eager to dive right into FreeAgent’s codebase, which I learned in the first week is a monolithic structure with thousands of files. This was a big change for me, as I was only used to my university assignments having 10 or so files at a time. This meant that my best friend during my FreeAgent internship was command F. As I mentioned earlier, I benefited heavily from Ruby being extremely readable. When trying to figure out how to implement a feature, there are a ton of examples in the codebase there to help out. 

For me, university has been much more individualistic. More often than not, I have held the sole responsibility for the projects. Here at FreeAgent, on the other hand, the most enjoyable part of my experience has been the pairing sessions, where I’ve had the opportunity to ask a lot of questions – and I mean a lot! And through collaboration with my coworkers, I get a unique insight into how different engineers take such different approaches to solving complex problems.

At university, my experience has been that the main focus is getting the expected functionality to work, rather than the code being maintainable. A big part of having maintainable code is actually not the code itself but the documentation. Rather than in extensive comments, the significance of good documentation is found in commits and pull requests. Interns  are naturally only short-term workers and we will not be around in a few months to explain our code. Therefore, our descriptions need to be well-written: explaining the problem at hand, focusing  on the why rather than the how, and making sure that commits state any side effects or other unintuitive consequences.


When I was 17 I had an opportunity to get an insight into one of the biggest civil engineering consultancy firms in the world. What stood out the most to me was one of the employees saying that the most important part of a bachelor’s degree is not the course itself but the summer internship you take. With all the lessons I have learned over the summer here at FreeAgent – from learning how to use a version control system like Git to the process your code has to go through to get deployed – I now know why.