Personal Finance: A Standard for Internal Enterprise Apps

Introduction

Personal Finance is an application designed to provide insights into a user’s spending as an individual, and allow the user to create realistic, practical budgets and future scenario projections.

Personal Finance was essentially created to fill a very personal, specific requirement, but as time went on I realised it also had the capacity to be a showcase project for what I consider to be a standard, internal enterprise grade, microservice application.

In the beginning I was performing finance planning by hand, I had a series of Excel sheets with graphs and calculations to pull insights out of data which I manually downloaded from my online banking and pasted in for each month. It was an enormous pain. The first version of PF was designed with the specific goal of simply replacing this.

Web Front End

A Showcase App

The brief for the app is nothing new or extraordinary – and definitely nothing that hasn’t been done before – however, after building some momentum on the project, I realised it had the potential to encapsulate improvements to my practice over the past few years, and to become a showcase application for what I consider to be the minimum standard for enterprise grade microservice applications.

In this portfolio piece I’ll walk though the features of PF but also use it as a jumping off point to talk about building applications, big and small, for large corporations.

You can read my more extensive thoughts on what I define as “enterprise quality” on this post. Originally these were the same post but I have a habit of writing far too much (don’t check the word count on this one, don’t worry it about it), so this post will focus just on the application itself, with some scattered references to the other post.

How to Read this Post

This post is split into two sections, a Phase 1 and Phase 2. This is because there was an initial build-out of the application centred around an initial goal that eventually ended, back in 2023. While I did the odd bit of work after that the project was mostly shelved, until the end of 2024 when I came back to revive it and take it in a new direction towards the place it’s in today.

The post is a bit of a long one because I’m not capable of brevity, so I’d recommend skimming the main headers to read about the interesting bits or just look at the images, rather than read it start-to-finish. Some of the best bits are towards the end.

Or maybe you’re the kind of person who reads technical manuals on holiday like me, in which case, enjoy!

Phase 1 (2023)

Data as a backbone

The core of the original starting point was to create a system focused around ingesting data as easily as possible.

The thinking was pretty simple; the most labour intensive, non-automated part of using The Old Excel Sheet was manually getting the data, plugging it in, categorising the transactions, and changing the graph ranges. The process was the same every time, the charts never needed to change, the slowness was in just getting the data in.

If the problem of data ingestion was solved – that is, made as smooth and automated as possible by a new application – most of the friction to automating insights would be removed.

If this could be achieved to an acceptable standard then the application would become a sort of CMS for serving standardised, sanitised transactions on which any number of features could be built. The features themselves were not really in focus at this time (more on this misstep later), because if the data ingestion isn’t there, then the app fails it’s primary purpose to replace the manual spreadsheet.

With this in mind, the guiding design principles setting out were as follows:

  1. Avoid being influenced the design of other similar applications, to avoid going down rabbit holes building non-priority features because someone else did it and it seemed cool.
  2. To focus the user journey on data import, making adding data as easy as absolutely possible.
  3. To make the application a showcase of my idea of a modern React application (for 2023/4/5).
The "Upload & Edit Transactions" page showing some past transactions
The upload page with some past transactions

Designing the data ingestion

Broadly speaking there were two big challenges to tackle when considering how to ingest data into the system:

  1. How to deal with various CSV formats, common in the export of online banking systems.
  2. How to solve the problem of categorising transactions into the user’s defined categories, making them useful for analysis.

Interestingly, transaction categorisation is a continually important line of research and development within banks, I’ve personally had some very interesting conversations with teams within NatWest on the topic.

Importing is done through an upload modal which allowed me as a developer to introduce data sanitisation and post-processing in between the two steps of the user uploading and writing the new data out to the database.

CSV relation mapping

The relation mapping with some fields set

The import modal presents a ‘relation mapping’ modal which defines associations between the columns on an uploaded CSV, and the column names used by the application. The user can change these mappings and see the data reviewed in the table in real time, allowing them to take any CSV format and auto-fit it to the application.

This feature is based on the idea that most people will typically perform batch-uploading, and will typically be taking data from the same online banking system, therefore the CSV mapper saves it settings to the browser’s locals to rage to prevent having to enter it fresh each time.

The staged transactions are presented to the user in a table under the headings used throughout the application. The user can immediately see where a piece of data is missing, update the column mapping, and see the result populate the table.

An addition made in Phase 2 was to allow each field to be user-edited, as well as allowing them to enter and delete rows manually. Checkboxes were added to each row to allow users to selectively ignore part of an uploaded CSV. Other Phase 2 additions included shrinking the row padding considerably to fit more on screen at once.

Context Reducer Pattern

A design pattern I can’t get enough of in React is the Context Reducer pattern.

React’s Context API allows you to define a local state which descendant components can access, and is intended to avoid excessive prop-drilling and callbacks. The useReducer hook provides a Flux State implementation within a React component wherein you have a piece of state, you update the state via a reducer, and you interact with the reducer via dispatching actions.

By leveraging these two together, you can effectively make a ‘mini Redux’ localised within one part of your application. Within a sub-tree of the application components can all subscribe to parts of a shared state, regardless of where they sit, and update that state via actions which can be moved out of the component for cleaner code.

In the case of the Transaction upload, having a single context gives us the advantage of staging the original raw data in a shared state, whilst small updates are made one at a time to rows within the table. The column mapping also lives in this state and is accessed at render time by the table to identify which piece of data to show in each cell.

One downside I must admit is that because the types of each column in the CSV cannot be known, each cell interprates it’s data as string | number | null | undefined which caused no end of headaches trying to maintain type-safety. The transaction rows were typed as as follows { [key: string]: number | string | null | undefined } so when the column mapping says that “description” can be found on the key “Transaction Desc”, it has no way of knowing what that will correspond to, and has to be ready for it to be undefined, an invalid number etc.

Category Matchers

Back in the days of the Old Excel Sheet, I got so tired of manually categorising transactions that I began writing a switch function to identify common strings in the descriptions. As you can imagine this got wholly unwieldy almost immediately and was one of the key motivating factors to building this system.

The equivalent this time round was to have an entity associated with Categories called a Matcher. Each Matcher has a string, which it uses to create a regular expression and evaluate each description at runtime and attempt to auto-assign a Category. The user is presented with the option to match the beginning or end of the string, a partial (global) match, or the whole string, and can choose whether the matcher is case sensitive.

For those transactions not able to be auto-matched the user could use an Autocomplete to find and select an appropriate transaction. These would be indicated by a little orange dot to draw the user’s attention.

Transaction ‘Quick Add’ Drawer

It became quickly apparent that creating Matcher’s primarily needed to happen during the upload phase, as this is when users will sit with the transactions open and when they will notice that their local Tesco Express shows up as “TESCO STORES 6257” and could easily be matched with a Matcher set to: text = ‘tesco’, case = ‘ignore case’, and match location = ‘match anywhere’.

Having to leave the upload modal and loose any staged data, navigate to the Category page, add a matcher Matcher, then come back and start again was clearly not practical every time this happened.

To solve this, a slide-in drawer was added to present a minified view of the list of Categories and allow users to add a Matcher (or edit/remove one if they notice a bad match) without ever leaving the submission modal.

When the user clicks on a transaction description, the modal opens with a compact list of Categories, then when the user clicks a Category, the sub-menu for matchers opens and pre-populates with the description and a set of default values for the other fields. This was all made with the idea of minimising the effort required to create a Matcher, in theory as little as three consecutive clicks.

Lastly, once the Matcher was created, the drawer would close and re-scan the entire dataset to try to perform an auto-match on all Transaction. So in our Tesco example, when I add the missing Matcher it will then auto-categorise all Transactions from that same shop.

The goal of these features was to try to bring the number of manual categorisations to zero, as an aspiration.

In reality, there will always be anomalous Transactions and things like online retailer transactions that will have to be manually identified by the user etc. However an insight drawn from The Old Excel sheet is that a majority – often a vast majority – of Transactions were form the same places and looked very similar.

Issues with the table library re-rendering

Another challenge I ran into was with Tanstack React Table. Nothing against React Table; its still my go-to for most use cases and I really appreciate its headless nature (after having to deal with the non-headless AG Grid at work), but I think this was definitely the wrong usecase for it.

On several occasions, most notably when I introduced the MUI Autocomplete selector for Categories, the table would get into a render loop. Adding in memo’s helped, but often just pushed the problem elsewhere. I started moving cell rendering out from the column definition and just manually identifying the column type which was a tad messy.

In Phase 2 I removed React Table entirely and create a ‘switch component’; a component who’s only task is to render one of a series of sub-components constituting the table cells. The table would still use a column definition like React Table, but instead of using the library itself, the switch would choose a cell type to render based on the column name.

Questions about data recall

Aside from being ‘the place where you upload Transactions’, the Transaction page had a bit of an identity crisis as it wasn’t initially clear what its purpose should be.

There was nominally an idea that it should show historical Transactions, the most recent first, but this was in conflict with the application state which may have a historical range loaded which was not the most recent. Should the page load up its own list of Transactions, just to have something to show? Should it override the cashed Transactions in state? This problem wouldn’t be properly addressed until the creation of the useTransactions hook, and associated conditionallyRefreshTransactions thunk action later in Phase 2.

The page ended up with a date range selector which would default to the past month, but also a Slider range control. Quite honestly I can’t remember how or when each came about but there was no clear idea which should do what.

The app was originally built around loading up the entire set of historical transactions from the database at once to perform analysis on (see ‘Mistakes’ below), but this was clearly not performant or scalable in the long run.

The Scenario System

The scenario modelling system was the first big ticket item I wanted to make, intended for the dual purpose of extrapolating past trend data into the future, and positing “what if” style variations to test the long term effect of hypothetical spending / saving actions and events.

At the time I found myself saving for a deposit to buy a house and was not only interested in when particular targets would be met, but wanted a better way to estimate what eventual running costs would look like over time. As an illustrative example, at the time (early 2023) there was a lot of fluctuation on interest rates so the purchase price had to be balanced against what the repayments would look like as it was entirely possible that I could afford the deposit but then be unable to manage the resulting monthly payments. I was unable to take one number as a ‘hard cap’ to target.

The initial idea was to have two halves to the Scenario system; past data would be used to extrapolate a trend line of how the current status quo is expected to continue, while a future ‘anomalies’ system would allow the user to imagine future changes and compare their results.

As it turned out, extrapolating a base trend line from historic Transaction data automatically proved to provide too many false positives to be useful. One unusual one-off purchase would throw the trend line way off, so this idea was mostly shelved.

Instead, I decided to just use the forward projection capability for everything and ignore past data for the time being. If the future projection capability was good enough then projecting from the current status quo wouldn’t really add any value. Also, the idea of just projecting past trends into the future is imprecise and makes allot of assumptions about how the user’s real life works.

Scenarios, Schedulers, and Transactors

The scenario system works around an entity called a Scheduler. Schedulers, represented by a series of classes based on the same base class define a single type of transaction (roughly corresponding to Categories but not constrained to them). These are then composed together by a system like the Future Projection chart to define a series of changes to a starting value over time.

The Scenario system was designed to be extensible, but initially focused on the Future Projection chart, which does exactly this; it defines a start date, a starting value, and an end date. Between these two dates it maps one or more Scenarios in order to create a line per Scenario representing the account balance through the time period, which would then be compared together.

How Schedulers are used to create transactions

Schedulers all contain an action function called a Transactor who’s purpose is to modify a given starting value, representing a single transaction, and is usually just a simple add / subtract. In this section “transaction” refers to a single date produced by a Scheduler, telling the runner when to modify its base value.

Schedulers expose a series of common methods which allow the Scenario to generate a series of transactions across a date range. These methods allow the runner (like the Future Projection chart) to query a single transaction to see if it is valid for a given date, or to generate a range of transactions within a given time period.

Scheduler’s date attribute

Schedulers are all initialised with a date which defines their base starting date. In repeating Schedulers this defines a starting date from which to continue a repetition from, if the Scheduler is not a repeating type, the date defines when that transaction occurs. Most of the accessed methods then also specify a ‘start date’, and some specify an ‘end date’, which are used to get a single transaction or a list of transactions for a range.

OK lets break all that down, imagine you’ve setup a weekly Scheduler to represent a weekly food spend, maybe you have its Transactor set to randomise the result a bit to mimic slightly varied spends across a month. You could set its start date to be a Monday from three months ago, and set it to repeat every 7 days.

Now, when you begin a Scenario in the middle of this week (lets call that Wednesday the 5th) to run for two weeks until Wednesday the 19th, you would generate a list of transactions which would yield a list of dates of Monday the 10th and Monday the 17th, instead of on each Wednesday (the 5th, 12th, and 19th) which is what we would expect if we were simply counting 7 days form the start.

In this case, the Scheduler will have looked at its date as the original start date, repeated forwards until it found the method’s start date, then counted each date which occurred until it passed the method’s end date.

Scheduler methods

Not all methods exposed are used by all Schedulers, simply because their different behaviours have differing requirements, the ones marked with asterisk’s (*) are the ‘main’ methods and shared by all:

  • increment*: Takes a start date (defined by the caller), increments it by one step, and returns the new date. This is used extensively by other functions to generate a range of values by stepping forward one increment at a time.
  • getRange*: Takes a start and end date and produces a list of transactions which fall within those dates (inclusive).
  • getMonthRemainder*: Returns a list of transaction dates for the remainder of a calendar month based on a start date argument.
  • getLastIncrement: Used exclusively by ScheduleByScalarTime. Takes a start date and tries to find the most recent valid date based on the internal start date and step size, stepping forward until the most recent date before the requested start is found.
  • getCurrentMonth: Used internally. Gets a list of all transaction dates for the whole calendar month in which the start date is found.
  • getMonthTotal: Used internally. Returns a list of a month’s worth of transaction dates based on a start date by incrementing a JS Date by 1 month.

The types of Schedulers created for this initial build-out were as follows:

Schedule by scalar time

This is probably the most versatile and commonly used Scheduler. This represents any repeating value which repeats based on a set amount of time, for example “every 13 days”.

This Scheduler is instantiated with a start date, representing the first point in time from which this date applies, and a step, representing the time period over which to create dates.

Schedule by Specific Day

This Scheduler takes a day of the month and repeats that on a monthly cycle (e.g “every month on the 20th”). Aside from Scalar Time this is anticipated to be the most useful scheduler as it can be used to represent repeating bills and costs that maybe occur randomly but accounted for at the end of the month.

Schedule by Day of the Week

This Scheduler takes a day of the week, specified as a zero-indexed number, or a small string (‘mon’, ‘tue’, ‘wed’, thu’ ‘fri’, ‘sat’, ‘sun’) and repeats on that day every week.

There is an option to provide an “nth day” parameter which would skip dates depending on the number provided. E.g. specifying 2 would execute on every second day. The intention with this was to replicate the functionality found in calendar applications where you could specify for example “occurs on the 3rd Friday of every month”.

Schedule by Event

This is the ‘one-off’ scheduler, representing a single transaction at a single point in time. Most of the methods exposed simply perform a check to see if a given date falls within the 24hr period defined by this starting date.

For example, the getRange function will return a list with one date in it, the class’s start date, if the date falls within that range. This is just to maintain compatibility with the other methods as it is acknowledged that using this class will be rare.

It should be noted that this class only takes a single date, there was an idea to create another class which would take in a list of specific dates but this was never implemented as there was little need for it.

Integrating Past Data

There was a vague idea early on that the Future Projection chart could integrate historical transaction data in order to compare a projection line against a real trend. The idea was that that a Scenario could be setup, placed to start somewhere in the past (for example the beginning of the year), and compare the accuracy of the trend line produced as it extends into the future.

As the feature developed I somewhat shelved this idea. There’s still provision in the code to bring the historical data back, but requires more work to really be useful and more than just a gimmick.

To be useful, the feature would have to save a future projection in order to recall it later and compare against transactions which have happened since. In this case, a user could set an intention at a particular point and then actively compare progress towards a specific goal as time goes on.

Real World usefulness

The really powerful part of the Future Projection chart was not so much holding oneself to account against a past goal, but the fact that it would dynamically change and respond to whatever today’s staring number is. Its use-case as a test bed for future plans is its strength, not setting an intention and holding oneself to it later on, a function that would later be filled by the Budget system.

Another way to look at it is this; if you had set an intention in the past but something unexpected came up in reality and through you off that plan, what use is seeing the old projection of how things used to look vs having the chart adapt in real time and set a new vision going forward?

Indeed, for my own personal use, the ability to pitch different scenarios against each other turned out to be the entirety of the value; I wasn’t using it for serious budgeting, I was using it to play with the overall trend.

While saving as much as possible for my deposit, I was faced with a sense of guilt when spending money on anything that wasn’t deemed absolutely necessary, a sort of ‘austerity mindset’ imposed on my own life that’s just not helpful over long stretches of time, life can’t be permanently on hold just to reach a goal x percent faster. But how should I think about essential vs non essential spending?

In one such example in early 2023 I was looking to buy an e-bike conversion kit (within UK legal specification) to upgrade my old folding mountain bike, which at the time would have cost about £420. I was struggling to gauge the relative cost, I agonised back and forth about it about it. Was it worth it? Should I just wait? The almost 0.5k cost had me very hesitant, if I was aiming for say, a 20k savings goal then that’s a whole 2.5%! Could that really be justified right now? Should I just wait, for the greater good?

I setup a Scenario to reflect me real earnings / outgoings as accurately as I could, I then forked it and added a Scheduler representing the ebike kit. I loaded both the “ebike” and “status quo” Scenarios into the Future Projection chart and splayed them out for a year.

The effect was jarring. Yes the initial dip looking a couple of months ahead was noticeable, but the eventual point of reaching the savings goal was only moved the slightest amount. That looked like a big dip right now, a ‘reckless setback’ that was surely not justifiable! just shrunk into the horizon as the x-axis grew, to the point where the lines barely looked different.

To make sure I wasn’t being biased I played around, bringing the end date closer, pushing it out further, but it made little difference in the long run. All of this completely re-framed the purchase in my mind. I bought the kit, installed it and it completely changed my life at that time.

The point of this anecdote is to illustrate the sense of perspective that I was ultimately aiming for with this project. Looking back the purchase would have been justifiable even if it was 5 times the cost, I could not believe how much I had hesitated, but this is the difference between real long term planning, and something that feels right to do now.

ORM

For the back-end setup this time round I wanted to create a truly headless API following as many best practices as I could to support a suite of front end consumers. In the post thus far I’ve outlined the web app front end, but I also had a stretch goal of creating a smaller React Native mobile version and a “proper” desktop app with Electron (or similar) to ensure the API was truly a microservice.

I had been using a lot of object relational modelling (ORM) frameworks such as SQLAlchemy at work and really wanted to try out a JavaScript framework in the form of Objection JS, which is built on-top of the query builder Knex JS.

I’d actually used Objection before with the Float Tracker and Scratch Card Tracker, but when these were made in 2021 I hadn’t quite got the idea of what an ORM was yet, the implementation was messy and didn’t make real use of Objection.

Comparing Objection to other ORMs

Data agnosticism

Something I really appreciated with Objection this time around is how database agnostic it is. With other ORM’s I’ve worked with you had to be very precise in specifying column type definitions and were often somewhat vendor-locked by having to use a specific dialect package. Perhaps if you were working on a more complex collection of databases than a bog-standard Postgres instance and SQLite this challenge would arise again, but I found it a huge relief to not have to worry about getting specific column types right.

Custom methods

A pattern I’ve picked up from other frameworks is to create custom methods directly on the model to encapsulate repeatable pieces of query logic. For instance, normally I would like to create a little utility to output a JSON serialisable object, in order to customise the response format in multiple consistent ways. In other frameworks I’d have a method called something like toJson() on each model (inherited form a custom base class), in which I could omit ID columns, change the column names, standardising the date formats etc.

It took me a while to cotton on to this, at first I was trying to modify internal values using the methods $beforeInsert, beforeInstert, $afterInsert, and afterInsert, but this is of course not the right way to do it.

Another thing I tried which was incorrect was to use the columnNameMappers static property to modify the way the model represents itself. This is not wrong per say, but doesn’t achieve the same as my custom JSON functions. For example, I’d need to interact with an ID column in the route handler logic, but then didn’t want it exposed to end-users. The result was a combination of somewhat unwieldy mapping objects that I could be abstracted to a separate functions.

Relationship definition

One thing I do very much prefer Objection for is the way it handles relationships. Objection’s relationMappings property is simply a delight to work with, it takes the often times thorny issue of complex SQL relations and joins and makes it readable and extensible. The verboseness of which you can specify a ‘from’ and ‘to’ column on a particular join, and the conveniently named relationship types are so easy to work with. I also enjoy having the ability to name joins to selectively use them later, it adds a layer of clarity to what data you expect that sometimes other frameworks just don’t give you.

Database Schema Design with Knexjs

Knex is called a “query builder” but I suspect that’s just because there isn’t really a convenient name for what Knex actually is. In conjunction to Objection’s models, Knex offers a powerful array of static and instance methods to tun SQL queries very succinctly.

However, one of the more powerful things Knex does is manage the underlying database connections, and provides the ability to migrate and rollback changes incrementally (read about migrations here), as well as providing seed data. Knex migrations are worth looking into in detail, for my use-case I leveraged them to build out features incrementally, allowing me to know straight away if something I’ve introduced is going to cause problems with the stable system as it stands so far.

Again I feel the need to compare to other frameworks. Some ORM’s I’ve used also have the ability to populate a database with their table schemas, using the model definition as a template to create the tables. Often this is in one “big bang” and can be a headache if you need certain dependencies to load in a certain order, and obscures when a change to the “schema” (in reality just the model) occurred. For that reason I don’t really consider using them for this purpose to be production worthy, as you never know what you’re going to get from ‘under the hood’.

By contrast, Knex migrations emphasise small, incremental changes, which (if you’re using it correctly) should always be backwards compatible with older versions of your tables. Combined with the ability to have seed data, this means you can really quickly – but in a controlled manor – tear down and spin up database instances for testing, which I found invaluable while trying to move fast and build out the backed.

I was also conscious that this is really the first of my projects that I personally want to use long term. The fact that Knex migrations are so safe and can be rolled forward and back means that I can be confident that my “mini production” environment, running on my local machine, will never encounter issues when I continue to build on the app and I’ll never need to start over and re-enter data.

Response Standardisation

Something I’m very keen on is the need to standardise API response formats for a variety of reasons, but mostly to make sure that the server has the ability to define response success messages and error codes, allowing the front end to handle failed requests elegantly.

The response structure is as follows:

  • req + res: The express JS request and response objects. Request is presently unused but placed as passive provision.
  • payload: Any response-specific data under a single key.
  • message: An end-user readable message describing the action taken.
  • statusCode: The HTTP status of the response as a convenience when debugging more than anything else.
  • error: Optional short-form error code pinpointing the type of failure.

In the case of an error, the message attribute becomes the human-readable error message. Read more about errors in Error Standardisation bellow.

In the past, I had used a set of standard response function in Python apps which allowed me to set a series of default responses keys and overwrite them. The idea was that, although it may seem silly to have a 404 and 409 response function when really any could be used, the fewer attributes that had to be specified at run-time the better, as it reduced potential for failure and made the route handler code easier to read.

A big part of what made this work was named arguments in python, in JavaScript I had to settle for argument lists and passing “null” where I wanted to use a default. I think next I’ll move them to use a single argument with an object, and rely on Typescript to require some attributes and conditionally override others.

Linting to underpin QA

This deserves it’s own post but I firmly believe that a robust linter setup is absolutely mandatory to write clean and performant code.

It also has the benefit of rapidly speeding up development time. By utilising IDE integrations, you can write incredibly messy code, paying no attention whatsoever to formatting, trigger a document format, and have clean, standardised, predictable code, which allows you to get to the value far quicker than putting in all those pesky like breaks yourself.

I use the ESLint: Fix all auto-fixable problems command provided by my IDE with the appropriate plug-ins, its always the first command when I click Ctrl + Shift + P to open the command menu and completely formats the document in line with a robust set of standards extended form the AirBnB config. The action of doing this is so ingrained and is such muscle-memory at this point I find myself doing it even on word documents.

I like this particular config because it’s rules and auto-fixes are very well written, and its good at doing things like truncating lines that are too long, and adding trailing commas to allow quick attribute list re-ordering, making complex function chaining look somehow intuitive and obvious.

Linters are not just there to provide standardised code and let you be lazy, there’s a testing strategy theory that I subscribe to that depends on them.

The Testing Pyramid

Kent C Dodds runs a fantastic blog with a range of invaluable insights into automated testing, which have provided a signifiant contribution to my team’s automated testing strategy. In this post he outlines the rational behind The Testing Trophy; a diagrammatic way of thinking of different types of tests intended to replace the older Testing Pyramid.

The Testing Trophy not only places much greater emphasis on integration tests than end-to-end or unit tests, but also crucially posits that linting and static code analysis underpins all testing. Linters allow you to identify a majority of common bugs and fix them directly within the IDE including many potential security risks and bad-practices.

Far from the perception that some more junior engineers may express – including myself in the past – that linters are like nagging parents trying to slow you down and get in the way of The Real Work, I firmly believe that with the proper workflow and tools in place, a robust linter setup actually increases the pace at which you can work.

Linters make things like unused and undefined variables, incorrect method calls, missing syntax immediately unavoidable and work to rid you of the ‘silly’ bugs that stand between actually writing the code and executing it all the way through. A good set of linter rules make your code predictable meaning that the time taken to even think through how you will make a change is reduced, the same way that walking up even steps is var easier and faster than uneven or misshapen ones.

This is broadly speaking why I also think that Typescript should be the default for any JavaScript project unless it is absolutely not viable. In regular JS you must deal with types, with arguments that have a particular shape, values which have multiple types, potentially omittable values and so no. The difference with using Typescript well boils down to the fact that you just can always just “see” what these things are better.

Building with Typescript from the ground up is like making a promise with your future self, that what you are defining now is accurate and reflects how things will be implemented. It removes the need to consciously remember what each bit does, where a function is used, what arguments it expects to receive, effectively pouring all that out into an “external brain” which handles much of it for you.

This also helps build documentation automatically. Documentation is another absolute must for me and removes so much effort from development time, even if it is nothing more than inline doc-strings and function annotations. Personal Finance is setup to automatically create docs with JSDoc, which is great but useless if the inline doc-strings are not well thought through. Ultimately the number one person helped by doc-strings is you, after you’ve long forgotten what you were thinking when you built whatever it is you’re looking at.

As the expression goes, When does code become “legacy code”? As soon as you have written it.

Component Framework

That DIY aesthetic

I’ve taken a complete 180 on the concept of component libraries / component frameworks.

If you read other posts of mine such as The Float Tracker you’ll hear me not just criticise the framework I was using, but condemn the very concept of a component framework. I would say “they’re all terrible, I don’t think its possible to make one that doesn’t introduce needed friction and badly opinionated components.” and “why should I let someone else do what I can do myself“.

There’s certainly a time and a place do something yourself, but not only does that begin to drag, it becomes very difficult to maintain to a high standard. It is very difficult to manually style components and account for every edge case, and be consistent about hover effects, and everything else that is needed to create something which looks professional enough to meet modern standards for “enterprise quality” application design.

I’ll take this moment to reiterate that the type of app design I’m talking about here is not objectively better. This is not about trying to make The Best App, just about the quality expectations of an internal enterprise app.

A criticism I might have levied back in 2022 was that “component frameworks will just make everything look the same and give it a sterile corporate feel”. In this, I would have been somewhat right! Component frameworks do need to be heavily customised in order to look unique to suit your style, but in this case “sterile corporate feel” is sort of what we’re aiming for here.

No rough edges allowed

15 years ago, the idea of MUI’s Autcomplete – which combines the utility of a filtered search menu and a drop-down list with sorted content and developer-defined matching strategies – might have been an advanced novelty only seen on large websites that were willing to invest in good developers. Now, my local kebab shop’s menu site has them.

There is a degree of complexity and user experience that is just expected of applications now. Sure, it would be a fun challenge to implement every single utility of a component library yourself as a challenge, but little UI glitches and styling issues that are likely to show up simply bring down the look and feel of applications which you do not want in a professional context, to say nothing of additional time.

In terms of theming, maintaining a large corpus of styles in a large application easily becomes unwieldy, and other tricks that my fellow devs use to get around writing CSS, such as building everything with Tailwind, is simply unprofessional in my opinion.

Material UI

For all of these reasons I’ve used Material UI as the backbone of this project.

MUI is an implementation of Google’s Material Design and I like its wide range of useful utilities, strong theming system, and it offers a wide range of customisability to adapt it to a given use-case. The widespread adoption makes it ideal too as there is a large community to get help from and raise issues fast.

Side note: if you look closely as you’re browsing the web, you’ll notice a surprising number of websites using MUI, usually with only a few small ‘tells’ such as the collapsing input labels which act as placeholders when the input is empty.

Most real-world uses-cases of MUI seem to disable some of the more “Googly” effects such as the ripple effect when clicking on elements, but it has been my observation that it is sometimes useful to leverage the association most people have with the Google suite of products by leaving that stuff in, it makes them thing your product appear slightly more established and refined due to bias transfer.

Yes, no framework is perfect, frustrations and limitations are inevitable, but to my mind if you are set on making a solid production-ready app to a high standard, they are mandatory, along with a well thought through design system.

Phase 1 Retrospective

I always like to include a retrospective section in my portfolio posts, a chance to say what went well, what I might have avoided if I was doing it again, and also to comment on the wider context in which each piece sits.

In this case, I’m going to continue working on some of the ideas outlined throughout the post, as well as a backlog of other vauge, early ideas not mentioned here.

As I mentioned at the beginning, at this stage in my career I see less value in showing off little apps, favouring instead proof-of-concept’s and learning that doesn’t necessarily produce an output. This application was intended to be a single, continually evolving demonstration piece of the type of work I do out of sight, and so therefore it makes sense that any new ideas be reflected here, rather than on a new project each time. For this reason, “Phase 2” doesn’t really have an end state and so wouldn’t really be suitable for a retrospective.

On the other hand, the stuff I’m here calling “Phase 1” very much did exist in a particular context, and did have an end point. I’ve therefore included a retrospective section here to talk about my reflections on it, and what lead to the shelving of the project until Phase 2.

What Went Well

The Future Projection system worked for its purposes

Despite how harsh I’ve been about the Scenario modeller system, it was certainly a success. Yes I took the long route to get there; about 70% of the application work was not needed to get to the point of producing the forward projections, but the work to make it extensible and adaptable is still solid and will be useful with future features.

The use-case I mentioned regarding the ebike purchase was in reality only one of many times I used the Future Projection chart to aid my thinking and plan ahead. And regarding the idea of holding oneself to a plan produced, I did manage to create projections which eventually turned true, so that’s a mark in favour of it.

The Upload system is solid

Despite all the trials and tribulations, the work put into getting the upload system to where it is paid off significantly in Phase 2.

When developing features we try to test and use our systems as we go along, but there’s nothing quite like approaching a system as a real user to get a task done, it changes the mindset you’re in and forces you to look at it in a different light.

When the time came to try an actually input over a years worth of transaction data I found the upload user journey worked like a charm, despite hesitation that I would discover it worked in theory but not in practice.

Yes I identified bugs and improvements to address in Phase 2, but the overall goal of making the upload as smooth and as easy as possible was achieved. In fact, it made me frustrated at the process of downloading CSV’s from my online banking, which was now clearly the slowest part of the whole process by far.

Focus on MVP

When I eventually got my various goals in focus and shook off my apprehension about its future state I had a second wind which enabled me to really focus in on what parts of the various features mattered and which could wait, and where I should simply put in passive provision for a later effort.

I think that if I had managed to achieve this clarity right from the start potentially I could have saved time and not gotten as fed up with the system as I did.

The reason there was such a gap in development phases, leading the the Phase 1 / 2 divide in this post, is that by the time I had my future projection MVP I lost interest in the rest of the app. Every time I thought about going back to it it just felt overwhelming, like an area of the office which has been a dumping ground for “stuff to sort later” for so long that the thought of starting to tackle it creates a bloated feeling of overwhelm.

What I Would Have Done Differently

Not having a product plan / milestones

Despite what said above about how good it was to narrow in on specific MVPs, the time taken to ge tot that realisation was still a signifiant slog. I started building this project with the idea that once the upload and scenario system was in place, charts and analysis and visualisations and what not would simply leap from holes in the ground. That having the golden source data so easily available would lower the barrier to building features.

In reality, the effort required to overcome various challenges in the Upload and Scenario Modeller somewhat burned me out on the project, I was sick of looking at it.

Yes, later on the above assumption proved correct; the effort to get these parts right paid off significantly, but I think if I could have focused on a leaner product earlier I might have achieved my goals faster and had patience left to build of the rest properly.

Delaying the decision about how the data life cycle works

Through most of the build of the application I did not commit to a solid idea of how the whole data lifecycle of past Transaction data would work.

As touched on, the application would load up all historical Transactions on the initial page load which suited some use cases but not most others. I procrastinated on the decision of how to create a more dynamic load, and so hindered the development of these features. I was worried about potential performance hits if every component re-requested a subset of transactions, and was unsure how to handle multiple components looking for different time slices.

Changing to a more dynamic load would have had (and later did have) implications for all sections of the app, but it was clearly non-negotiable as overtime the older transaction data would bloat the application, and slow it to a halt.

It took until Phase 2 to really sort it out by way of implementing the useTransactions hook and the idea of the ‘transaction window’. While this took extra work its quite an obvious solution when you look at it in totality, I should have made this call earlier.

Using Create React App

RIP Create React App my beloved.

I have only respect for the authors and the bootstrapping they have provided for so many projects over the years. When I first started out building React apps outside of CodePen back in late 2017 I was bamboozled by Webpack and the whole idea of development environments, CRA provided an accessible on-ramp which enabled me to carry on building.

Despite not being officially shelved until a year or so after this project started, it was clear that CRA had stopped receiving updates for quite some time and was not fit for purpose even then.

This caused no end of problems upgrading the base boilerplate. Not only was there a complex web of dependencies to try to upgrade as far as possible, but CRA itself includes a series of utilities that get installed directly in the project, and were incompatible with later versions of the dependencies. These had to be manually reverse-engineered and then modified, or bypassed completely.

In particular, fixing the automated testing config almost killed me. I ended up bypassing the CRA testing config, no doubt missing out on some useful functionality, but it was the only way to get unstuck after a most frustrating weekend.

Phase 2 (Dec 2024 / 2025)

Motivations

The ugly duckling project

I confess I had this project hanging over me at since it’s soft abandonment.

I 2023 I bought a house, meaning that Personal Finance, for the Scenario / Future Projection use-case, had functionally achieved it’s purpose. Whilst this is of course a good thing, it made the idea of going back and putting in the effort to do something with even less appealing.

After Phase 1 I would return occasionally to do the odd bit of work on it but quite frankly the system, in its half finished and clearly flawed state, was an embarrassment. So I tried to burry it and move on.

I told myself that I didn’t need to do anything with it, that I should avoid acting on the sunk-cost fallacy. Yet under my floor boards I heard it beat louder and louder, plaguing my thoughts, a rhythmic drum that wormed its way into my thoughts. Would its abandonment mean I don’t follow through? Have I lost my ability to actually do this type of work, outside of the day job?

OK maybe these questions are a little dramatic but it was annoying me. The task of adapting it was not going to be easy, and on top of that, the various struggles I had with the system had put me off, just thinking of how to turn the project around made me feel a sort of bloated malaise.

A portfolio reevaluation

2024 was a good year for me overall, but very intense and very difficult at times. The bad bits got so bad at times that I began to re-evaluate my portfolio and CV, asking what would happen if I needed to jump ship. I found it was in a poor shape. The last project listed was The Scratch Card Tracker which had no content, just a placeholder message and a couple of images. It was three and a half years old at this point and while it was still pretty good, was clearly a far cry from the quality of work I do now.

Shades of GeoCities from this “page under construction” post

My most recent project, the Tickler System, was good but had many of the same issues as Personal Finance given the later was the starting point for the former, so it was also not ready for show-and-tell hour.

And so the frustration finally boiled over, in December of 2024 I became laser-focused on PF. I setup a Trello checklist and unloaded everything that was obvious and outstanding into it, and prepped myself for an intense sprint. While this task list is not exactly a fully built-out JIRA project, it was suitable enough to create a solid conveyor belt of tasks and milestones to “hard burn” through (as we now like to say at work due to my obsession with The Expanse in 2024).

Budget System

Thinking backward instead of forwards

A key part of this new revival was the Budget System, which would prove to provide a better backbone for the entire app than anything else created thus far.

When I was building out Phase 1 I was personally focused on the task of saving every penny I could to save for a deposit. In effect you could say I was in a ‘looking forward’ mode, only interested in potential futures on the way to this goal.

Coming in to this revival for Phase 2, I found myself settled for just over a year in my new place, comfortable in my spending habits, but wanting to know a lot more about what those habits comprised of, in order to plan better for the future. I was therefore much more focused on the task of analysing past transactions, and utilising the accumulated data in the system much more than the scenario system did. In this sense I was now ‘looking backwards’.

The Budget System leverages the existing Category entity heavily, effectively just defining a set amount of expected spend per Category, per month. These expectations are then retroactively applied across historical Transactions to find how those Transactions perform in versus the expectation.

When most people think of a “budget” they think of a forward looking thing, a commitment to themselves or others about what will be spent and where in the future. In this case Budgets could certainly be used for forward planning, but are primarily intended for retroactively analysing spend “performance”.

This was done to serve two main purposes:

  • Explain why a savings goal was missed / overshot, to take learnings forwards (find a more economical source of apples and drink less Tango).
  • To use this retrospective to create a real-life forward facing budget if desired. I.e. the more the Budget could be refined to fit past data trends, the better it would represent an accurate prediction of future spending.

Budget Breakdown vs Overview Distinction

An important idea early on was to split analytics to separate long-term trend analysis out from more detail focused drilling down. This was based on a personal observation about what kind of psychological headspace a user would be in for each of these tasks and therefore the importance of separating each.

Two pages were created to capture this: the Budget Breakdown and Budget Overview pages, where Breakdown would provide in-depth analysis, while Overview would provide a longer-term view.

It was clear from the beginning that interplay between these two pages would be important. A switch button was used in the bottom right of each screen to allow the user to quickly jump from one to another while preserving the selected date range. There was an idea to combine both pages and have the Breakdown functionality appear as a side-panel on the Overview page, but this was deemed to be a low priority ‘gimmicky’ feature and largely not needed due to the switch button.

Breakdown Page

The Breakdown page came first and was primarily intended to be used for analysis on a single month’s worth of transactions, but can be used across a multi-month period if desired.

Breakdown is focused on deeply interrogating the performance of key metrics. It provides various tools to identify outliers and anomalies, potentially to explain them away.

Headline Charts

At the top of the page are two leader charts, the Percentage chart and the Donut chart.

The Percentage chart shows a positive / negative discrepancy per Category, across the time period vs the Budget. It is intended to be the primary starting point for a deep-dive investigation, directing the user towards any obvious outliers.

The percentage chart allows the user to display spend by percentage of allotted budget, or by raw cash amount vs the budget. This allows users to look at the data more horizontally as a large cash overspend may be a small percentage and vice-versa, the user would have to be judge of what is important.

The Donut chart shows how each Category totals against the overall spend, again this is mostly just to put spends in perspective vs the overall Budget.

Transaction Preview Plane

This is a pop-up window which opens when the user clicks on a series on a percentage chart, providing a filtered table of the all the Transactions of that particular Category contained within the time period.

It highlights any transactions which fall outside of one standard deviation across all values, which proved to be a useful way of identifying most anomalies, without making assumptions on the specifics of what counts as “normal” for a given Category.

This aims to speed up the process by which a user can find unusual transactions and outliers to explain large discrepancies.

Glance Cards

Glance Cards are little snippets showing a single high-level metric ‘at a glance’. Examples include the ‘largest overspend by percentage and currency amount’, ‘total spend’, ‘total expected vs actual spend’, and ‘number of categories over Budget’.

Glance cards are primarily intended to build a narrative and act as commentary on the spend performance. They are all driven by the same standardised data format, provided by a higher order component, keeping compute times to a minimum.

Table view

The table shows effectively the same data as the percentage chart but all in the same row. The table highlights Categories which fall outside the user-defined acceptable variance, whilst also displaying that variance to the user.

This allows the user to identify ‘problem’ areas but also to dismiss false positives, for example if a category is only a little bit out of its accepted variance that may be deemed not actually an issue.

Aggregate charts

These are a series of charts that show how various metrics (similar to the ones used by the Glance Cards) accumulate over time.

Overview Page

By contrast to the Breakdown page, the Overview page is intended for longer date series, defaulting to the past 13 months (to include a complete year plus the current month). Like Breakdown, Overview shows metrics calculated against a month long step across the selected time period to find and observe variations.

The idea behind providing this view is that, yes individual months may contain discrepancies from the target, but what is the impact long term? Does each month need to be perfectly balanced and self-contained? Is a metric like ‘spend on gifts’ under budget for most of the year but with a large spike at a holiday season? Probably not something to worry about.

The Overview page is less interested on explaining away anomalies than it is at looking at a broad spectrum of spending to find commonalities and differences through time.

In the example of a gift budget that spikes at certain times of the year highlights the contrast provided by the Overview page vs Breakdown; Breakdown allows us to place anomalies under a microscope – and implicitly indicates that all anomalies are bad – Overview instead places these anomalies in a context which may change the way the user looks at them.

Where the Breakdown page asks the question “how and where did this result come from?”, the Overview page asks “What is the trend of these numbers? What is meaning of the trajectory?”

Percentage Charts

The Percentage Chart used in the Breakdown page makes a reappearance on Overview, but this time serving a time-based view, placed next to an array of other percentage charts to show change over time.

The calculation is the same showing a positive / negative against each Category, but this time the charts are much smaller and intended to highlight consistent large discrepancies vs areas where a Category discrepancy rebalances.

Aggregate Charts

Overview has its own set of aggregation charts, showing similar data to before but this time focusing on trend difference between months, rather than how the data accumulates.

Open / Close / High / Low

This chart was created to show a total value for the purpose of gauging a trend line, but taking into account fluctuations throughout the month, on pay day for example. It does this by using a candlestick to show the value at the beginning and end of the month, the trend up or down, and the minimum / maximum values.

The purpose of the min value is to help the user to create a comfortable buffer in the event they are drawing near zero (or in overdraft) during the month.

A Standardised Breakdown Data Type

While designing the first chart on the Breakdown page I realised the same computed data series could be used across various charts, either directly, or as a starting point to reduce another chart’s data transformation compute. The datum would hold calculated values of performance relative to budget, but is also combined with other attributes such as the Category’s assigned colour, in order to minimise lookup times.

The standardised datum contained these attributes:

  • colour: Allows us to associate a Budget’s Category’s identifying colour back on chart series’.
  • categoryId: Allows a chart to query the category if needed.
  • categoryName: Holds the category name for when it was needed for a chart series (prevents each chart having to look it up each time).
  • budget: The budgeted spend amount for the time period (multiplied by however many months were in view at a time).
  • spend: The actual spend value for this category across the time period.
  • diffFloat: The difference between budgeted amount and the actual spend value as a number (positive for overspend, negative for underspend).
  • diffPc: A value representing the actual spend as a percentage of the allotted budget.
  • variance: Two numbers denoting the accepted underspend and overspend, as a percentage of the budgeted amount. Percentages are used instead of spend amounts as no readjustment is needed if the user changes the budget value.

Acceptable Variance

In various sections of the Budget pages an overspend / underspend ‘out of bounds’ flag is added per Category, based on spend versus the acceptable variance defined by the user. The idea of this variance metric was to try to allow the user to quantify what amount of variance was cause for alarm / cause for note.

In almost all cases an underspend or overspend is expected; it would be strange to spend exactly what your budget specified month after month, and would defeat the need for a finance tracker really.

Typically, most people would see an underspend as a good thing; so long as it doesn’t mean you’ve forgotten to pay a bill then saving money is good, right? But consistently spending under a budget value could cause issues if it means that the budget is no longer accurate and should be adjusted.

Overspends are mostly considered bad for obvious reasons; but how much of an overspend really matters? For example, if I’ve calculated my transport costs to be a particular amount, they might fluctuate during the year as I take more trips when the weather is nice, or less when I’m ill and avoiding going into the office, so long as it balanced out over time there is no cause for alarm.

On the other hand, my fixed-rate mortgage should be exactly the same each month, so if I get charged any amount other than the exact amount I expect that would cause alarm.

Percentages vs Raw Values for Anomaly Detection

In many parts of the Budget pages we alternate back and forth between displaying anomalies as percentages vs float amounts.

With a metric like food and drink spend – especially if you include treats like takeaway food in there – you might expect an amount of currency over/under spend, up to a hard maximum. This might not be a large percentage discrepancy if you’ve already budgeted a large currency amount.

For costs like monthly subscriptions, you’d want to be aware of price increases but the actual currency amounts may be very low (at least for me). A £10 a month subscription going to £11.99 is the difference of £1.99 which is minimal, but represents an almost 20% increase which I may find not worth it over time and decide to trim my commitments.

For the Budget Percentage Charts on both Budget pages, I made sure to allow the chart to toggle between percentages and float amounts, with some interesting outcomes. Sometimes what looked like big anomalies turned out to be trivial amounts of money in reality, other times subtle shifts in monthly commitments were hidden under small currency amounts.

This also allowed vastly outsized but expected anomalies to be safely ignored. For example, it seems my entire family have birthdays in May which makes the percentage overspend go through the roof on that month, but for other times in the year its very low. I can switch from percentage to a float amount and then use the actual spend value to determine if I’m happy with the amount spent.

On the Breakdown page this percentage discrepancy might show up as a bit bright warning, but then on Overview you’d see this contrasted against much smaller spends throughout the year.

State Management

Personal Finance uses the latest Redux standards which involved a couple of shifts in my thinking, namely relating to the use of thunked actions and Redux Saga.

Thunks replacing Sagas

In the beginning I was using a Saga observer pattern to handle most complex actions, based on a standard we were using at work. This involved handling all API calls by triggering a handler saga per request which would then perform error handling, multiple response cases etc.

Later on, thunk actions replaced all of these as I found there were annoyances to do with the way that Redux Saga ties in with Redux Toolkit and issues with type inferences with Typescript. When yeilding the result of a promise funciton call, in my case API calls, the sagas were unable to automatically infer the type of the response. Mostly though, thunk actions are just easier to use when you need to compose multiple together and are much easier to trace errors from.

I think there’s probably still a place for Redux Saga, but for the vast majority of every day tasks, Redux Toolkit can handle everything within itself and without the drawbacks of Sagas

I’ve not migrated the early sagas as I think its an interesting showcase to have the side-by-side, but for later Redux Actions I exclusively used thunks.

Cached Query Pattern

The cached query pattern is a simple but useful for manually controlling the request / re-request of data in situations where RTK query is not appropriate.

Firstly, we define a top-level store key which represents one entity or data type, for example one for Categories, one for Budgets, one for Transactions etc. Next, we include the following keys within that area as a starting point:

  • loading / loaded: To indicate the status of the load query.
  • response: The raw section of the API response object which holds the item, useally an un-ordered array.
  • timestamp: A millisecond timecode of when the last refresh was completed.
  • Various ordered lookup objects: Depending on the entity type, I like to post-process API responses into one or more ordered lookup objects, for example a key-value lookup by ID.

This pattern isn’t suitable for everything, but I’ve found it especially useful in the past to handle caching of data where sporadic lookups will be required. By creating a series of ordered ‘lookup’ objects the application can be used to very slightly increase initial load times for the purpose of saving lookup time later when components need to dynamically query from state.

This is based on anecdotal and studied evidence that suggests that users are far more likely to notice lag during the runtime of an application, vs a slight increase in initial load. By doing the work upfront we can benefit from the fastest possible dynamic lookup by using objects with the entities ordered by ID, name, relationship to other entities etc.

To implementing caching, dynamic hooks and thunk actions are created such as the one demonstrated here, which block re-requests within a particular cache period unless the component specifically requests to force a refresh.

Now of course, this assumes that dynamic lookup is necessary. If an entity does not require this kind of treatment it is still far better to use RTK query. In my case, all entities on Personal Finance fall into one of two camps; those that benefit from this treatment such as Category, Transaction, and Budget, and those that don’t need to be dynamic at all because they’re not expected to change all that much, such as Card.

The Transaction Window & Transaction hooks

The solution to the question of how to dynamically load transactions mentioned in Phase 1 was to define a small starting window to load up, and then to allow individual pages to conditionally re-request a range of transactions using one of two strategies:

  • Th Blunt Option: A new time range could be defined and the whole list of transaction re-requested.
  • The Expand Option: The time range could be expanded, and transactions queried to ‘fill the gaps’.

This was labelled “the transaction window” and was intended to be a half-way stop between simply re-requesting a range of transactions on every page change, and loading everything. Which strategy was used depends on the type of component is requesting them.

Some components are intended to be smaller consumers of data, subscribing to a small range of transactions, but potentially conflicting with other components on the same page which have totally different ideas of what they want to do. In this case, two components could request two separate ranges of data and the smallest start date would be combined with the largest end date to create the transaction query.

For example, perhaps today is a day in August and the window defaults to May – July. Component A may be contented with this and find its date range within this range, in which case a refresh would not occur. Component B however wants to see April – May only, in which case it would create an action which would expand the window to April – July, pushing out the start date but maintaining the same end date.

In the second query method, the Blunt method, a larger page-level component such as one of the Budget pages could take control of the Transaction Window range and set it to a range of it’s choosing, before sub-components attempt to expand the window. This is used mostly at the page level to clear out stale data, effectively resetting the window.

This is handled by a combination of manually calling the Transaction request thunk, and the useTransaction hook.

useTranaction will take a date range and conditionally perform two tasks. Firstly, it queries Transactions from the Redux store within the requested range, secondly it dispatches a thunk action which will potentially expand the Transaction window if one of the dates lays outside the current settings.

Application Design

“Modules” idea

On of the original ideas for the application was a series of composable “modules” which would be arranged by the user on the home page to create a sort of dashboard.

The Future Projection chart and Past Data charts were built as ‘Modules’ and could adapt to being displayed as a small tile or re-used to for the full page layout variants of each’s corresponding page. In their Module form the main chart would be shrunk, the controls simplified and hidden behind an MUI Accordion, and the layout tweaked to remove padding, margins and extraneous elements which may distract from the main content. There was an idea that the various elements of the Budget pages could be split out in this manor, the components themselves were already designed to work together after all.

One of the big issues which halted the continued use of this idea was data re-computation; if Modules had to be self contained they had to perform their own data processing, and if many modules were doing the same processing then redundant compute time would potentially increase massively.

Potentially the issue of data re-computation could be solved by moving more components to query their data from a React Context as opposed to receiving it via props, this would allow the various Budget page components to work the same on the home page as on the Budget pages. However another issue arises about how much each chart would make sense in isolation, without the context of other elements found on the Budget pages. For example, the two headline charts on Budget Breakdown are designed to work together as a starting point of a deeper investigation, does this still make sense without the rest of the data? Or will they just look a bit stranded?

I think the eventual solution will be a compromise between the two; we can reuse ideas from various places in the app and Modularise them, but I think these new modules will have to be built as their own thing and not the literal component used in another section of the app.

The idea of dashboards such as this one are always appealing to developers, but I know from my own experience of them in a banking context that just throwing metrics at a wall and making “cool” visualisations is not nearly as valuable as taking time to think through and create one or two really impactful designs and do them well.

Overall layout

In terms of the actual layout and design of the application we’re not breaking any new ground but I wanted to highlight a couple of features I was pleased with.

The original layout was just single full pages with a static header containing the title and a hamburger menu which contained just a list. In Phase 2, a new set of sidebars were implemented utilising the same underlying dynamic layout but with a choice of how that sidebar would behave.

Discreet Layout

The ‘Discreet’ style sidebar is the default. When closed it shrinks to show only a set of icons for each menu section. For sub-menus, if the icon is clicked the sidebar will open out, shifting the content to the side, and then close again when the user makes their selection. When closed, a Tooltip is used to display the full title on hover.

Static Layout

In ‘Static’ mode, the sidebar opens above the content of the page, and when closed disappears entirely to give an un-obstructed view.

Common Dyanmic Menu

The underlying menu is driven dynamically by a common datastructure which then drives the content allowing for quick changes. This data structure allows each item to either render a link or a sub-menu which will display as an accordion, as well as setting its icon, title, etc.

Sub-menus can be set to default to open or closed, allowing the developer to play with the space available. There is a top and bottom section to group items towards the top of the menu or the bottom to make full use of the space available.

The purpose of this type of layout is also to allow passive provision for other elements to be grouped within the sidebar sub as popup menus, modal triggers etc, making it truly a dynamic part of the application instead of just a ‘list of links’.

When the transition was made to this new menu type, the old static list of page links was changed to better group pages under mode top-level titles for the purpose of aiding navigation but also to ensure that if / when the sidebar grows it avoids clutter.

Previous layout
  • All Historical Data
  • Budget Breakdown
  • Budget Overview
  • Manage Budgets
  • My Budgets
  • Manage Categories
  • Categories
  • Upload & View Transactions
  • Transactions
New Layout
  • Top:
    • All Historical Data
    • Transactions
      • Upload & View
      • Categories
    • Budget
      • Budget Breakdown
      • Budget Overview
      • My Budgets
  • Bottom:
    • Profile
      • My Cards
      • Settings

‘Acrylic’ Dark Theme & Phase 2 Theme Redesign

A dark colour palette was chosen to be the default this time round, simply to make better use of a range of Category colours but also to ease the burden on the eyes when closely scrutinising data. Inspiration was taken from the “Acrylic” design theme, namely in the header where a soft semi-transparent blur is used.

In Phase 1 I was a bit too timid with my choice of palette and was consistently bugged by contrast issues, namely the ‘primary’ purple colour against the soft blue background. In Phase 2 I severely strengthened and boldened each of the colours, and defined a new ‘secondary’ colour (used in areas of notification and element highlighting) in the form of this light blue.

When selecting the new colours I looked to create a slightly neon vibrant aesthetic, partly to follow current industry trends, partly to imbue a sense of energy that was sorely missing with the old duller, more matted colours. I was also by websites like GitHub that use the same effect to emphasise contrast.

Lastly, I was very keen to choose a “good” white colour; I have dyslexia and find that some website’s dark mode has very bright text on very dark backgrounds which makes the letters quite sharp and much more difficult to differentiate from one another.

An invaluable site to aid in this redesign was MUI Theme Builder by Brian Reynolds https://bareynol.github.io/mui-theme-creator. Its a little out of date so should be used as a guide rather than a full theme-builder-exporter, but is still really helpful. When I am staring new projects, I wouldn’t just export the config and plug it in, but instead start with a blank theme in my project, test out ideas, and incorporate them into that.

ResponsiveLayout higher-order component & the useContentWidth hook

As part of my commitment to consistent style and layout I wanted to create a higher order component (HOC) to be used to layout any full-sized piece of content on a page to a set of standardised sizes.

The ResponsiveContainer HOC provides a simple wrapper for page content and adjusts its max-width and min-width to ensure that the content is not too small on narrower screens, and not too large on wider screens. To compliment this, I introduced a custom hook, useContentWidth, which would evaluate the screen with and provide a few outputs to determine what size content could be, which ResponsiveContainer used under the hood, but can also be used stand-alone.

The hook uses MUI “breakpoints” to provide a standardised set of widths in response to the screen width. I have a particular bug-bear for continually shifting “truly responsive” layouts; you don’t need to have a dynamic layout for every screen, instead I think it is better to define rough boundaries for where each size should be used and work to those, which is the idea behind both of these utilities.

The hook has several outputs to be consumed:

  • breakpoint: One of the values “xs”, “sm”, “md”, “lg”, “xl” used internally to define the steps at which the screen size triggers a new content width setting.
  • contentWidth: A number value for the current content width.
  • windowWidth: The current window size.

Common list layouts

Another utility that helped speed along development was a common list utility comprising of a layout component, to render the list / grid items, and a set of controls.

The list layout allows the parent component to chose which layout options are allowed, as some might not be appropriate to some use-cases, but also holds the state as the behavior of the items in the grid itself may also need to change, as demonstrated by the Category page here.

Error Boundary

Two Error Boundary strategies were used as part of this boilerplate, the first being a higher order component placed at the root of the project and taken from the React doc’s suggested implementation. This component is intended to be a fallback and only used to avoid white screen errors in places where a manual catch has been missed.

The preferred method of catching errors is with another higher order component, the ErrorMessage. ErrorMessage and the associated ‘error’ state capture errors thrown and display a standardised error modal anywhere in the app.

The error messages themselves consist of these pieces of information which broadly link into the standardised API response format from earlier:

  • title: The top-level title broadly stating what has failed.
  • message: The user-readable description of the error, intended to provide the user with actions to try next or context to understand why this has happened.
  • error: The short-form error code or message intended to help the maintainer narrow in on where the error originated.
  • timestamp: The time the error was produced.

In addition to this, the stack trace is also captured but is not presently displayed.

The error state can be manually triggered from within the application, or automatically invoked by way of the intakeError thunk. intakeError is an action which first attempts to identify the type of error and then customises the error model behaviour based on what it is. It then attempts to pull out relevant information using a cascading list of sources, writing this information to the state. This process looks like this:

  • First, the thunk tries to match a network error as there are specific authentication actions we want to take here:
    • If the user has authentication has expired, we don’t display any error, other logic will have provided the needed redirects and token refresh attempts.
    • If the error originated from the back end, we want to extract the error details sent by the API as part of the standard response format.
    • If the server has sent a HTML response, it is likely that NGINX or perhaps the network has failed. We try to identify a HTML response and manually set a ‘network error or server failure’ message.
    • If none of the above is true, but the error is still an AxiosError, then we pull out the expected Axios error response format.
  • Next, if the error is not a network error but is still a standard JS error we pull out what details we can, overwriting a set of default messages.
  • Lastly, if for whatever reason the thunk has been invoked with something other than a JS error (for example an incorrectly thrown promise), we display just the default ‘something went wrong’ message.

JWT authentication

For this boilerplate I decided to go with a JWT based authentication system, due to popularity amongst enterprises for internal API federation, and horizontal scalability as a stateless authentication system. In particular I was keen to leverage the token refresh pattern to allow a user to return after a brief break and be automatically re-authenticated.

I admit I was sceptical at first. As I setup the various parts of the auth system it felt a bit too close to violating the rule that had been drummed into me when learning about auth; “don’t implement auth yourself”. However, by following an established pattern and making sure to use robust remade solutions wherever I could – in particular where encryption / decryption is concerned – I could be assured that I was safe as I was really just composing prebuilt elements, same as any other auth implementation.

A series of util files were created to wrap the encode / decode functionality as well as for hashing passwords. An Express middleware was created that could be added to any protected route and would check the validity of the auth token, injecting user details into the ‘request’ object for the endpoint to use.

Auth token refresh was implemented by defining a second, longer lived token, which can not be used to interact with the API but can be used once to query a fresh auth token, within a window of about a week. To solve the risk of token replay attacks a exclude table is used holding IDs of revoked tokens, when a refresh token is used its unique JTI ID is placed in this table. On subsequent attempts to use the same refresh token and for the main auth token on every API request, the JTI is checked against the exclude table and the request is rejected if it is found to be revoked. A cron job is used to clear out expired tokens from the blacklist table once a day.

The useToken hook provides a series of functions to handle the conditional re-querying of the authorisation tokens on the front end when the user’s auth has expired. On the application’s mount it first attempts to retrieve and valid a stored token from localstorage, if this fails it attempts to invoke the token refresh logic, and only if that then if it fails will it direct the user to login again.

A boundary component wraps pages that require the user to be authenticated before using them. If the auth state is unconfirmed, the boundary will redirect the user to login.

An auth ping is periodically used to check the authentication status prior to making any request which may be bounced due to an expired token. This provides another opportunity for the token refresh logic to be invoked and allow subsequent requests to succeed.

Lastly, a lockout flag is used to ensure that multiple token refresh attempts are made at the same time. Because of the mitigation on replay attacks, multiple attempts to use the token refresh endpoint with the same refresh token will trigger a ‘hard’ failure on any of the subsequent requests meaning that the user will receive a 401 unauthenticated and need to login again, even though they had a refresh token.

Implementing an auth system also allowed the defining of “cards”, representing a single account or cash pot, to which all transactions could be tied.

This solved a major issue with the Phase 1 implementation where the question of how to deal with multiple pots was left unanswered. I had floated the idea of combining multiple accounts into the same view, trying to make the application understand transfers between them, allowing it to ignore savings account transfers for example, but this was impractical vs just making sure that switching between accounts was easy.

You’ll note that in the seed data I’ve included a Category called “Savings” (or “Account Transfer” in some cases). This is a mirror of a Category I use to fence off intro-account transfer and ignore it in analytics. Most of the time my account transfer transactions are just moving money to a savings account.

Localisation

Something I was really keen on getting right this time is localisation.

To claim that this is a “production grade” app while not supporting multiple locales and currencies simply wouldn’t make sense. Yes, enterprise organisations will typically only support one or two languages that they use internally (unless they are truly global), but to my mind the extra work needed to make an application localisation-compatible invalidates any excuse for not doing it.

Intl browser utility

If you have not already heard of this browser utility I’d highly recommend you check it out as soon as possible. Stabilised in the last 5 years by the looks of it, the Intl utility provides a range of localisation static functions natively within the browser.

Number Format

One of the most useful Intl utilities for Personal Finance is Intl.NumberFormatter, which allows you to transform large numbers and currency values to a language and currency specific native format.

For example, in the past if I had wanted to display a large currency value I might have used a function with a large regexp to insert commas into a large float value, capped the number of digits after the decimal, then tacked on a currency icon at the start, and called it a day. But this is inadequate as it has a few problems. Firstly, not all languages use decimals and commas in the same way, secondly there are right-to-left languages that may be in use, and thirdly the currency symbol is not always placed at the “start” of the number, some currencies place it at the end.

By using Intl.NumberFormat I can pass in a locale, a currency, and a number, and get a nicely formatted, correctly localised response every time.

For Personal Finance I created a custom hook, useLocalisedNumber which exposes two functions, currencyLocaliser, and numberLocaliser to be used within components to auto-transform numbers based on the user settings for Language and the currency denoted for the transaction (or from settings).

Locales

The package locale-codes provides a useful list of both language codes but also local codes, allowing the user to select between, for example, en-GB and en-US. This turned out to be important to use versus a hard-coded list of locales, as the format of the numbers produced by Intl.NumberFormat will depend on the specific locale, not just on language which I encountered when I tried to use the standard list ISO 639 codes.

I18next

Translations for all text contained within the app are provided using I18next and the React plugin, react-i18next. The delightfully named ‘t‘ hook provided allowed you to easily inject alternate text content depending on the users settings, meaning true translations can be provided by developers rather than clunky browser auto-translations.

While only UK English is provided to start (alas I am only a monolingual Brit), the setup allows anyone wishing to extend it to simply add a new translations file.

The layout of the translation file uses a combination of text grouped by entity (e.g. Category or Matcher), and common share text pieces. Sometimes a short-hand code is used, other times a “literal” key is used, where the key corresponds directly to the English spelling. To try to formalise this system I created a contribution guide within the README.md but almost certainly there are better ways of handling it.

Dockerisation

I decided that this time I would not be hosting a demo version of this project due to lack of good hosting options but also from a risk perspective; I don’t want people to try to plug in their real financial data and to be liable for it.

While I have no interest in commercialising a publicly available production application, I still wanted to design a hypothetical deployment strategy as part of making this a realistic demonstration project. I wanted a way to say “this is how it would be productionised if I wanted to”.

I also wanted to have my own long running version to use day-to-day, a “mini production” which I would maintain like a production release but is just running on my local machine for me.

Lastly, because I wanted to have the front end be potentially just one version amongst other cross-platform implementations, I didn’t want to have a single demo project which grouped the Web and Service together too tightly. Having a single demo would be nice but would require prioritising the current Web app which I might later decide against.

Using Docker with the default Postgres image was the natural choice to conveniently package the various components, without requiring a user to install additional dependencies on their machine.

Backend

Dockerising the backend was relatively easy to start with, but I had a lot of trial and error to find a way to satisfy two desires:

  • 1. How to support the three build modes I had defined, “development”, “staging”, and “production”.
  • 2. How to reduce the build steps by using a single docker-compose file for the API and database.

Many initial attempts used multiple docker-compose files and the docker merge capability which produced some bizarre results, namely grouping my API and database under an Auto-generated container name.

Next, I had to consider when to run Knex migrations. On one attempt my combining both the Postgres image and my own API image meant that the Knex migration was attempting to run prior to the database starting up and therefore failing the build process. The reason for this was that the build process was only interested in waiting till the container was up, and didn’t care that the database itself was still booting.

Seed Issues

Another unexpected issue was with he seed files I was using to re-build the database each time, it turns out that Knex runs seeds in alphabetical order based on the file name. This was causing issues because SQLite doesn’t care about constraints like foreign key columns but most other databases of course will; I it was necessary to make sure and insert the Card before the Transaction that references it etc.

This was solvable by making new seed files using the Knex seed command using a template which would inject a millisecond date code, ensuring that this would dictate the order of execution, not the entity name.

Lastly, there was some confusion about how to handle Date formats in Postgres but most of these were solved by switching to ‘datetime’ columns and adding a bit of extra config to the Knexfile.

Web Front End

The initial idea for the front end was to have a build placed inside a static folder on the API as part of my bid to just have one docker compose file, but this never quite felt right for a couple of reasons:

Firstly, it violates the microservice pattern by combining front end with back, it introduces a versioning issue as it becomes hard to remember which has been committed. Yes there are ways around it but see the next section for why I was hesitant.

A bigger problem was that the API router could not differentiate what was a static file request, and what was an API request. Yes I could have prefixed all static resource URL’s with /static/, but that would have then introduced issues using it in any other environment and I just wasn’t really up for doing that.

An example of coupled front-ends from work

On a project at my work there was a need to incorporate the build of some applications into another app due to the time it would have taken to approve entirely new separate apps (or properly integrate them). This would involve building the sub-app, and then committing the build as a static part of the codebase of the host app.

To try to do this as elegantly as possible I devised a method of creating ‘build IDs’; unique identifiers created at build time which would be written out to a .txt file within the build as a form of traceability. After this the builds would then be placed within a folder of the host application called “ecosystem-apps” which then had sub-folders for each environment.

When making changes a pull request would be opened on the application’s repository, another would be create on the host app’s repository. The two would then be linked by mentioning the build ID and adding hyperlinks. This worked for what we needed and did – in theory – provide the traceability needed for secure change release control, however it is a far from perfect solution.

Because both repositories are tied together but still separate, reversing changes proved to be difficult. Then, if there was a feature awaiting sign off on an ecosystem app project, it may be completely unrelated to all the other apps of the host but will still block the change releases as the host app needed to maintain its own linear sign-off release train. In other words if one app was still in test but not ready to go, the rest also had to wait. It was possible to only build for one environment at a time but it very quickly became difficult to tell which build for which app was then subsequently built into which environment on the host.

It was an effective solution to resolve the dilemma we had at the time but I would not recommend it if it can be avoided.

Static file server

The solution of course was to simply find a static file server solution using Docker. Its Docker silly.

Following some online guides I created an NGINX image and adjusted it slightly; most tutorials had the build copy everything from the source project into the server, I changed it to just take the output build/ folder.

Lastly there was – no presumably no-ones surprise, not least my own – a CORs issue. The NGINX image had a set of server headers which it attached to each response, namely Sec-Fetch-Site, and Sec-Fetch-Mode. I added a custom nginx.conf file and overwrote these headers but it turns out you can’t overwrite server headers by default, instead this little snippet must also be included.

Et voila! A containerised web app, and a containerised API bundled with it’s own Prostgres database.

AI Integration

Of course something else has changed since early 2023.

A new swath of new AI technologies have sprung up based on the transformer model, first galled “generative AI” and now just “AI” and have seemingly appeared integrated into everything.

I have another blog post brewing about my thoughts on the current moment; how we talk about, think about, and fantasies about “AI”, a term I put in quotes not because I reject it, but because right now I think its best understood as an exercise in branding.

Many commentators including myself, both working in a “tech” field and otherwise, have avoided saying too much at any given time about AI / Gen-AI, partially because by the time we’ve finished writing the context for the post is out of date, but partly also because (at least in my case) we don’t want to make the wrong bet. We hold back on being to certain or too strong in an opinion for risk of saying something that will look widely naive, or overly pessimistic, or lacking in some other future facing analysis, in 2 / 5 / 10 years time.

In the case of Personal Finance, I’m somewhat in the position of being unable to avoid ‘making a bet’. As a supposed flagship portfolio piece my including or not including AI features is inherently a statement. Do I build an AI feature into it, and risk it looking in 5 years like I jumped on a temporary fad, ignoring the politics and climate at play in five years time? Do I avoid building one, and then look like I missed the boat, took my finger off the pulse, acted stubbornly, and tried to ignore an inevitable change?

From my experience being involved in building NatWest Group’s internal general use AI assistant ‘Aiden’ it would have been relatively easy to tie in various LLM API’s, build a Retrieval Augmented Generation (RAG) system and provide user insights, maybe chain these together to build an ‘agentic flow’ like Aiden has. I thought about creating a little applet marketplace, with pre-formed prompts (ChatGPT calls these “GPTs”, Aiden calls them “Aidlets”), and allowing users to make reusable prompts.

But as soon as I imagined the little sparkle emoji button – the consensus iconography to represent the current “AI” brand, the little chat bot window popping up to offer some unsolicited opinion, the cutsy name I’d have to give it, and all the added visual noise I simply couldn’t justify it.

Even if it ‘worked’; i.e. provided some added functionality and effectively showed off that I can navigate this type of design, it would have felt too much like a gimmick to warrant putting my name on it.

Automatic Commentary Generation

However, I did still want to have my cake and eat it too. So I asked myself what a hypothetical AI integration could do and some way of testing how this might look in order to have at least say I had done it. I honed in on the idea of automatic commentary generation as this felt like the most appropriate and de-risked form of LLM integration.

Personal Finance is really just about looking at data in as many different ways as possible. Its guiding design principle is an idea to build a theoretical user-friendly OLAP cube, letting the user tossing it in the air, bounding it off the wall, lick it etc. just to see how the data looks from all angles.

On commentary generation, it is presently up to the user to decide what the data means. PF does not offer any targets, savings tips, suggestions to move money, criticisms for ‘bad’ behaviour, or anything else that might constitute commentary, because that would be pushing an opinion (or opinions) on a user, and making assumptions about what they value.

I think that integrating an AI assistant or any kind of AI analysis along this line would ultimately violate this principle and would create a monotone perspective that would get tin the way of the user rather than helping them.

I did have the rather amusing idea of an “AI council”; I thought that if I could get a range of LLM systems with different “perspectives”, or maybe a range of performance metrics on different types of analysis, I could then pitch them against each other and provide a chorus of perspectives to help the user generate ideas. Unfortunately this isn’t really feasible given the costs and energy waste considerations, but also because I don’t think the various systems on offer do too much different in terms of performance on this type of task.

Despite this, I was still curious about how a current LLM system would respond to the task. I loaded up the following prompt (which maybe isn’t the best but I think is an OK starting point) and gave it to a range of LLM services. I won’t post every result but have included this one from Google’s Gemini 2.5 Flash model as I think its a good representation of the types of responses I got.

I included 3 months worth of anonymised data but have only included the first few rows bellow to save scrolling.

LLM Prompt

You are a financial advisor bot. You are designed as part of an application called Personal Finance.Personal Finance allows users to upload transaction data exported from their online banking applications and provides a range of analytical tools to allow the user to explore their data from a range of perspectives.Personal Finance is oriented towards insight and commentary generation, helping them to explain the "why" behind what is happening.You will aid the user by generating analytical commentary. You will keep your responses formal and speak with a trustworthy tone.You will comment on the supplied input and try to find useful patterns. You will try to spot insights that the user has missed.If you do not find anything worth commenting on say so; do not just speak for the sake of speaking.If it is useful, ask the user follow-up questions to gain additional insights.Do not tell the user things they may already know.The user will provide their exported data in CSV format bellow.USER INPUT:id,currency,cardId,userId,date,transactionType,description,debit,credit,ballance,createdOn,updatedOn,categoryId,categoryName8deb4842-23c9-4d75-9521-b132ea9337ad,GBP,be913800-df3b-4285-803a-88e971fde8f3,dc4b572d-1be4-412f-b99a-4cc947e9f048,1752006522807,DEB,NorthernRail,2.3,0,1013.4,1752006522807,1752006522807,b6945bdd-04b6-4df9-9530-1e1a9ce273c3,Travelc458ab9c-13b8-42b6-adab-9cc1ccdacbc2,GBP,be913800-df3b-4285-803a-88e971fde8f3,dc4b572d-1be4-412f-b99a-4cc947e9f048,1751920122807,DD,CTAX,65,0,1015.7,1752006522807,1752006522807,11002146-43f2-45a8-91f8-f718b5f34de3,Utilities1bd22e58-9804-4242-9f3a-1886766ec911,GBP,be913800-df3b-4285-803a-88e971fde8f3,dc4b572d-1be4-412f-b99a-4cc947e9f048,1751920122807,DEB,CEX LTD - new Pokemon game,42.95,0,1080.7,1752006522807,1752006522807,3d84b0a8-f4b2-4464-96ac-43e8babd2156,Hobbies7c13eed4-5e5a-4a9f-ae24-0c9222bd2438,GBP,be913800-df3b-4285-803a-88e971fde8f3,dc4b572d-1be4-412f-b99a-4cc947e9f048,1751920122807,DEB,Spotify Limited,11.99,0,1123.65,1752006522807,1752006522807,7364d968-1058-48dc-bdbb-c8268a97bdf5,Subscriptionsb70b071a-0684-42e8-a042-22b2520b829c,GBP,be913800-df3b-4285-803a-88e971fde8f3,dc4b572d-1be4-412f-b99a-4cc947e9f048,1751660922807,DD,SHELTER NAT CAMP,12,0,1135.64,1752006522807,1752006522807,4b8614e2-4f8d-41e2-8d62-7163eefa6812,Support & Donations...

LLM Response

To provide a comprehensive analysis, I will process the provided transaction data to identify key spending patterns, income, and any notable trends.Financial Overview and Spending AnalysisYour transaction data provides a clear snapshot of your financial activity, highlighting key areas of expenditure and demonstrating a consistent income stream.IncomeYou received one payroll deposit of £1987.35. This appears to be your primary source of income within the provided dataset. There are also two incoming payments categorised as Gifts from "Brother" (£12.99) and "MUM" (£37.00), likely reimbursements or contributions for joint gifts.Key Spending CategoriesYour expenditures are spread across several categories, with a notable concentration in a few areas:    Food (£462.68): This is your most significant spending category, accounting for approximately $\text{47%}$ of your total debits. You frequently shop at major supermarkets such as Tesco, ASDA, Aldi, and Marks & Spencer, along with smaller local Co-op stores. There are also entries for "PARK SERVICES CAFE" and "MUSEUM CAFE VISA", indicating dining out or leisure-related food purchases. Given the frequency and value of these transactions, this area presents the most significant opportunity for potential savings.    Travel (£49.35): This category primarily consists of frequent, small debits to "NorthernRail" (9 transactions totaling £21.70) and a single larger expense for "UBER *ONE" (£4.99). The recurring NorthernRail expenses suggest a regular commute or frequent short-distance travel.    Gifts (£188.70): You have a substantial number of transactions related to gifts, with descriptions indicating purchases for various family members and friends. These include items from Amazon, Oxfam, CEX, British Red Cross, Sue Ryder, Pets at Home, Greggs, and a whisky purchase. This suggests a generous gifting habit.    Utilities (£192.19): This category includes regular payments for "CTAX" (£65.00 each on two occasions), "Energy bill" (£110.00), "SKY INTERNET AND BROA" (£53.29), "Home Insurance" (£15.78), and "DIRECTLINE INSURANCE" (£17.40). These are essential fixed or semi-fixed expenses.    Support & Donations (£30.80): This category includes donations to "SHELTER NAT CAMP" (£12.00) and "SUE RYDER DONATE" (£10.00), as well as "NON-GBP PURCH FEE" (£0.50), "NON-GBP TRANS FEE" (£0.11), and "FREECODECAMP.ORG" (£3.95), the latter appearing to be a recurring subscription or donation to an educational platform.    Hobbies (£54.94): You have two notable transactions in this category: a new Pokemon game from "CEX LTD" (£42.95) and a movie from "AMAZON" (£9.99).    Healthcare (£6.76): This includes a "BOOTS/619" purchase (£4.36) and a "BOOTS 619 (floss)" purchase (£2.40).    Subscriptions (£23.98): You have two identical "Spotify Limited" direct debits, each for £11.99. This indicates a recurring monthly subscription.    Mortgage (£600.00): A significant direct debit for "Mortgage" is present. This is a substantial fixed expense.    Home (£11.95): A single purchase for an "Amazon.co.uk - replacement vacuum cleaner filter" falls into this category.    Bike (£20.81): A purchase of "AMAZON MKTPL - new bike tires" is recorded.    Phone (£16.20): A direct debit to "EE" is noted.    Clothes (£10.00): A single purchase from "BARNARDO'S 2931 - better coat" is present.    Dentist (£20.00): A single payment to "Dentist" is recorded.    Garden (£57.44): This includes "OXFAM - various gardening" (£19.27), "Thompson Morgan - seeds for new year" (£18.98), "AMZNMktplace - the expanse season 4" (£12.80), and "CASS ART" (£25.20). The "the expanse season 4" purchase seems miscategorised here and might fit better under 'Hobbies' or 'Entertainment'.    Dog (£10.99): A purchase of "AMZNMktplace - dog treats" is noted.    Work (£0.89): A small debit to "CLOUD LFPGEG" is recorded.Noteworthy Patterns and Insights    Frequent Small Transport Costs: The recurring "NorthernRail" transactions (9 instances) suggest daily or very frequent use of this service. While individual amounts are small, they accumulate to a notable portion of your travel expenses. Analyzing the dates may reveal a consistent commuting pattern.    Significant Gifting Expenses: Your "Gifts" category is quite active, with numerous purchases for various individuals. While generous, this is an area where conscious budgeting could be applied if you are looking to reduce discretionary spending.    Potential for Food Savings: Given that "Food" is your largest expenditure, reviewing your grocery habits, exploring meal planning, and comparing prices across different supermarkets could lead to considerable savings. The presence of multiple "Park Services Cafe" and "Museum Cafe" transactions also suggests opportunities to reduce spending on prepared food or dining out.    Recurring Subscriptions: The Spotify subscription is a regular outgoing. While not a large sum, it's worth regularly reviewing all subscriptions to ensure they are still providing value.    Utilities and Fixed Costs: Your direct debits for utilities, mortgage, and insurance are significant fixed costs. While these are often difficult to reduce significantly in the short term, it can be beneficial to periodically review providers to ensure you are on the most competitive tariffs.Recommendations for Further ExplorationTo gain deeper insights, you might consider:    Monthly Budgeting: Categorize and total your expenses on a monthly basis to see how your spending aligns with your income over a longer period.    Setting Spending Targets: For categories like "Food" and "Gifts," establishing a monthly budget can help you manage these discretionary expenses more effectively.    Reviewing Recurring Charges: Periodically check your bank statements for all recurring direct debits and subscriptions to identify any services you no longer use or that could be obtained at a lower cost.By understanding these patterns, you are better equipped to make informed financial decisions.

The response was decidedly vindicating.

I’d be keen to hear other people’s thoughts on this response and the prompt that I used, but in my opinion this is not a useful output.

I acknowledge that the prompt could probably be greatly improved, so I’m not taking any points off for things like the list of spending categories for instance, as perhaps some part of my prompt suggested to do this, but I will be specifically focusing in on the commentary which is mostly provided towards the end of the response.

On ‘travel’ I was disappointed that it did not recognise that the “Northern Rail” trips were not a commute, instead it just said ‘hey maybe keep an eye on that’. Same with ‘food’, there was no analysis of average spends per shop, just a generic ‘you seem to spend allot on this maybe think about that’. All throughout the commentary there were attempts at providing savings tips, but these suggestions offered little substance and assumed that outgoings reductions are the main goal.

Assumed aggressive saving strategy

On that idea of saving at all costs, I was quite surprised that it targeted gift spending as somewhere to reduce outgoings. I’m no financial advisor but I imagine that if I was and a client was comfortably in budget and had a “generous spending habit”, I would not see this as a problem that needed rectifying unless specifically requested to find any ans all savings possible.

Its line “While generous, this is an area where conscious budgeting could be applied if you are looking to reduce discretionary spending.” that had me most bemused. Hey nice naturism there buddy, but have you unconsidered making Line Go Up instead?

Opportunity for deducing context missed

There were a few ‘traps’ in the data that I was mildly hoping the LLM would pick up on and account for. For example, I’ve included a transaction at about lunch time most Saturdays at some museum or park based on my own habit of attending a sketching group most Saturdays.

I was hoping that the LLM would ‘figure out’ that this is likely a trend that will continue and is based on a regular outing such as the aforementioned regular meetup. There were also other takeaway food transactions inserted that I deliberately inflated the cost of, hoping that the system might say something like “I see that you tend to spend money on lunch most Saturdays, I’m assuming that this is a social outing that you’re not going to change? However I think its notable that you spent as much in 3 months on that as you did one time getting food delivered from ‘Pizza 50 miles away’. Maybe there’s more local restaurants that do the same thing for less?”

I tried to give the LLM permission to ask follow-up questions, perhaps it would ask about things like that and tailor its response, but in my experience they tend to ‘speak when spoken to’, responding to the last message you send with as close to a complete answer as they can give, not suitable for the kind of back-and-forth I wanted.

Lastly it finishes out with generic financial advice you might give to a child receiving pocket money for the first time.

Assumed values (or lack thereof)

This is really the key point here; I don’t begrudge the system for just summarising what it sees, commenting superficially on it, and wrapping up with spending tips, that’s effectively what I asked for by not specifying any goals or values or motivations.

In essence what else was it supposed to do? Take a really strong stance that I should invest more on riskier sources? Berate me for not eating out more often and enjoying life? Tell me that I could really afford to be giving more to charity so what’s this about a new air fryer and vacuum cleaner filter? Try to predict future societal events and tell me to stockpile bike tyres because its predicted a war starting that will affect the place where they mostly come from and make prices go up? Tell me that the financial system will collapse and your money won’t mean anything in 40 years time anyway because they economy will be radically different so maybe stop worrying about anything long term?

Ok I’m being a bit facetious here but the point is that there are infinite little calculations we’re constantly doing, maybe not even consciously, that affect our values and habits and influence ‘how the data looks’ that an automatic system – designed to be somewhat middle of the road by the vendor – just isn’t going to do.

Yes a better model could probably perform much better at analysis the patterns, but would be no more useful about finding meaning in the data without the context that the user would bring, with whatever mindset they happen to be in. For example, a better model might be able to perform the kind of reasoning I described with the Saturday lunch costs, but would that add anything? I already know that happens. I’m the one that does it.

I’m not ruling out that an AI system could provide any value at all, but at the moment it would be yet another case of making the use case fit the tool rather than the tool to the use-case.

The Float Tracker: Eliminating Cash Loss Through Data Vis | Waitrose & Partners

The Float Tracker system is an unofficial piece of software to track physical cash levels (also called “float levels”) and provide insights by graphing the data across time.

It achieves this by taking in data from various parts of the cash process systems, providing holistic ‘snapshots’ of till behaviours, contrasted with real world events.

The insights from the Float Tracker allowed us to reduce the overall levels of physical cash in each of the tills whilst actually increasing their ability to deal with unexpected surges in trade.

This post is part of a wider series discussing my various activities as Senior Cash Partner at my retail branch to overhaul operations, eliminating losses, and designing & implementing new processes.

This is a long and context heavy post. You may want to skip to the bit about the outcome.

Context

Disclaimer

This post concerns my experiences working in a retail branch and includes discussions of sensitive data and branch operations in order to contextualise the featured project. All information discussed has been carefully sanitised to remove identifying details as much as possible, to completely anonymise individuals and to change some details to appear authentic while being fake and representative. This post written in good faith and should not be taken as representative of real branches, people or organisations.

When I first took on the role of Senior Cash Partner at my branch, I inherited a cash office run by one person (who had just left), 13 cash-accepting tills (5 staffed tills and 8 self checkouts) with weekly cash losses in the hundreds, sometimes thousands of pounds per till.

We would loose hundreds of pounds worth of National Lottery scratch cards every week, numerous prize-draw tickets. Some of our old-style self checkouts (SCO’s) would “eat” notes, sheading them to pieces, cash would be swapped between tills casually by users without the system knowing.

Paperwork was disorganised, investigations were near impossible to conduct or even know where to start, and it turned out that till operator training was more a game of Telephone than a formal process.

The person I was replacing was highly skilled and intelligent, but they were a jack of all trades across the branch and so unable to dedicate focus to the issues listed. It needed someone to focus in and take ownership, making the section “theirs”. The problem was just assumed to be unsolvable.

Lets employ a bit of Socratic method to delve into the issue.

What is A Till?

To understand how a retail unit tracks and deals with cash losses / gains (called simply ‘discrepancies’) its useful to understand a few key bits of jargon and processes.

Once again, the information here is provided to give context and has been generalised to remove sensitive data and organisation specific details. Other retail units and shops will differ but most supermarket chains in the UK can be assumed to have generally similar processes.

Tills come in different forms, these are as follows:

  • Mainline till staffed tills with a conveyor belt and dedicated packing area. Suitable to use with shopping trolleys and only found in the larger branches.
  • Kiosk till which are smaller, more condensed staffed units, with space for a basket and bag, usually serving restricted items like cigarettes, Lottery services, and Spirits.
  • Self Checkouts of which there are a few variants:
    • Security Scale SCO’s At our organisation these were the standard and are now being phased out in favour of more modern designs, the “Skinny SCO’s”. These are old noisy boxes, unintuitive to use with insensitive scanning beds (the bit that goes “boop”). Bing early generation self checkouts they are large, clunky, prone to breakdown and their bagging area security scales slow customers down, cause frustration and don’t have any real benefits.
    • Cash Accepting SCO’s The same as the Security Scale machines but with an extra unit added on the side to accept cash. These can still be found in some branches but are mostly being phased out due to unreliability.
    • Skinny Checkouts Newer units which have no security scale and do not accept cash. These are more streamlined and take up much less space both in terms of breadth and depth. The screen and scanner are mounted on the front casing which can swing upwards to access the computer and a very small receipt printer. These are mounted in line on a continual platform that is simply cut to length.

The COVID 19 pandemic accelerated the phase out of the old style SCO’s not just because of the unhygienic nature of physical cash, but also the sheer amount of time the attendants have to spend maintaining them; unsticking stuck notes and coins, resetting the security scale, all of which restricted customer flow and inhibited social distancing.

I could write a whole post on the psychology of SCO’s and insights based on observation from a couple of years of working with them. If you’re interested in the psychology of checkout design, have a look at this report by my former colleagues at the Design Against Crime Research Center.

We went from having 5 staffed Kiosk Tills and 8 large Cash Accepting Self Checkouts to an array of 12 cashless Skinny Self Checkouts (Skinny SCO’s) and three cash-accepting Kiosk Tills. Once COVID-19 hit, the middle kiosk was taken out of service and remained so until I left the partnership one and a half years later.

The new layout diminished the amount of cash payments slightly, but a compensating effect meant that the remaining cash activity was intensified and concentrated on the remaining kiosks.

How Do Tills Deal With Physical Cash?

Where cash accepting tills are concerned there three ‘types’ of cash held within the unit:

  1. Loose Coins: Held in the cash drawer (the bit that pops open for the operator to give change), separated by denomination.
  2. Notes: Usually adjacent to the loose coins in the cash drawer are the notes used. Typically these are separated by denomination but depending on the configuration of the drawer they may just be bunched together.
  3. Bagged Coin: A bulk reserve of bagged change kept in a secure location allocated to each till. Each bag holds a standardised amount of each denomination.

Should the till run out of a denomination in it’s loose coins drawer, it can be topped up from the bagged coin. Because the change is a reserve, the digital system draws no real distinction between Bagged and Loose Coins; it doesn’t know or particularly ‘care’ what the denomination levels are or how much is still bagged vs loose; its all part of the same lump sum. Remember this, it will become important later.

The digital system not only doesn’t discriminate between Bagged Coin and Loose Coin, but it doesn’t track individual denominations at all, it sees all cash movement as being an equivalent in 1 pence (the smallest GBP denomination).

Factors influencing cash flow in and out

This disregard of individual denominations seems counter-intuitive at first but makes sense when you think about it from a system design point of view. How could a digital system possibly know what combination of change each specific transaction is using? How could it know the breakdown of the cash handed over by the customer?

Perhaps I gave them a 50p piece, or perhaps I gave them two 20s and a 10, perhaps the customer found a 50p, gave it to me, and was given a £1 (an action you’re not really supposed to do but happens often).

There’s ways to calculate what the optimal change should be, but so many factors come into play, which frequently result in ‘sub optimal’ change being given, including but not limited to the following:

  • Customers requesting specific change (e.g. £1 coins instead of an equivalent note)
  • A particular denomination runs out
  • Incorrect change being given accidentality or by malicious actions by staff
  • Customers regularly not accepting small change which then gets placed back in the till
  • Found change on the ground being placed inside the till
  • People performing scams which results in them tricking the cashier into giving them more change.

When accepting cash, the operator inputs the value given to them by the customer, which in turn is used to calculate the value they’re told to give back to the customer, serving record keeping purposes. For an example, if the total is £17.32 and a customer gives me a £20 note, I’ll select £20 from the option list to tell the till what I’m placing into it. The till will pop open the drawer and request I give back £2.68. It doesn’t have any way of knowing how I made that value up.

It’s not uncommon for input errors to occur, meaning that these records are wrong, even though the till balances out in the end.

For instance, imagine I’m a customer purchasing something which totals £5 and I give you, the till operator, a £10 note. The input screen will let you type in the value or select from a list of “suggested” inputs, generated to save time typing “10.00”. You now accidentality hit £20 on the input screen suggested values. The terminal will tell you to give me £15 but you know this is incorrect and instead give me £5.

The record will show the wrong figures but the till is still balanced in the end and I’ve still received the change I expect.

Discrepancies

Discrepancies describe the difference between the expected value of a till and its actual contents.

The digital system tracks cash issued to each till, through it’s transaction records it knows how much cash it expects to find once totalled. By manually counting the contents of the till, we can produce a snapshot of this amount and compare them.

Actual Value - Expected Value = Discrepancy

Some amount of discrepancies are inevitable and often self balance; a little bit of change falls behind the till, a customer leaves their change instead of accepting it. A partner makes a mistake and gives too little change, a scammer makes off with some petty cash, some change is found on the floor and so on.

Typical guidelines state that a discrepancy in the range of -£10 to +£10 is acceptable, and that the +-£5 range is very good.

There are a couple of factors which contribute to discrepancies and compound issues relevant to cash processing.

1. A certain amount of discrepancy is unavoidable. Even with perfect actions from the operator (no human error), customers leave a proportion of change behind choosing not to take it.

Sometimes (not often) coins come in that are either damaged or are actually a different currency that slipped through. We got a lot of international traffic from customers coming in from the airports and off of Eurostar so both customer and staff mistaking a euro for a pound was a common occurrence.

Then there is the fact we do not have any operators who don’t make human errors, given that they are humans. Change falls between cracks, sometimes incorrect change is accidentality given, even the old SCO’s would sometimes create errors, this is the imperfect nature of cash.

2. There is no reliable way for the tills to autonomously report on their actual contents. Even if you fitted the cash drawers with scales, cameras, and other features which could auto-measure the amount of cash at any given time, and even if you could guarantee the accuracy and calibration of such technologies, a bad actor could trick the system easily using random objects, fake coins, and slight-of hand.

Then there’s the fact that such a magical self-reporting till would probably last a total of five and a half minutes in active use; despite appearances, a shop-floor is a highly abrasive ware-and-tear environment. Equipment needs to be durable, it needs to withstand constant use, hits and heavy loads all day every day, sensitive reporting equipment would not last.

There’s a reason that consumer printers are still, after so many years, notoriously unreliable. Machines that deal with physical mediums and lots of moving parts in this way require a lot of maintenance and consistency of the medium.

Think of the uniformity and quality assurance systems of an industrial packing machine, churning out tens of thousands of units a day, that require full time technicians to always be present, monitoring vital signs and performing repairs. The little coin scale we used to perform manual spot-checks will frequently de-calibrate and is susceptible to errors if something is touching it or leaning on it.

Considering how customers crumple, un-crumple, fold, exchange, sit on, tear, write on, warp in to non-Euclidean geometry, lick, drop, and rub physical cash over lifetimes that can span decades, the fact that self checkouts are able to run autonomously at all is a wonder. And autonomous, accurate reporting of cash levels is completely infeasible.

Reconciling Discrepancies

Now that we understand how to find a discrepancy, what can we do with the information?

Sessions are digital records which denote a period of time to which transactions are assigned. Typically done per week, sessions allow us to cordon off a start and end date in which to look at transactions in order to defined periods to perform investigations in, explain discrepancies and then draw a line under them providing a “clean slate” for the next block of time.

While it’s possible to close and open new sessions at any point during the week, a branch like ours will typically keep one session open for the entire week, reconciling the result at the end of the week.

This performs the basis of investigations (if necessary) and plans for improvements to hone in on issues, but it also provides a time frame for issuing new change to the tills.

In our efforts to tackle losses, our branch employed an optional operational policy, to perform a ‘spot check’ every day before trade opens. This provides a consistent data stream through the week showing discrepancy change day-to-day. Random Daily Spot Checks (RDSC) can be employed on-top of this to target problem times / days / individuals.

Change demands can be done at any time but are best utilised when they are spaced apart; a till is unlikely to change much between 1430 and 1500 but may be quite different between the hours of 0930 and 1500. The time taken to perform the check and the disruption of closing the till are the determining factor that restrict how often spot checks can happen.

Spot Checks

Spot checks allow partners to gain a snapshot of a tills state at any given time.

Spot checks involve manually measuring the contents of a till and producing a readout from the digital system including, amongst other things, the total expected value of the till’s cash levels. If done correctly, you have a verified data point for a specific timestamp and a discrepancy.

Spot checks involve shutting down the till until the count is completed and the readout is printed. If the till is being used while the digital system is being consulted then the expected readout will not match the actual count. Likewise, if the count has any sort of error, the entire spot check has to be re-done.

Given that spot checks can be done during trade, shutting down a till while customers are potentially queueing is not ideal. Spot checks have to be done quickly and efficiently, and if the count is bad then that time is considered a waste.

Spot Check Record Keeping

Spot checks vary between contexts and branches in terms of how they are performed. However there are consistencies in terms of paperwork which factored into the design of the float tracker.

Values are sometimes recorded on specific float record forms (or just on a strip of receipt roll paper) then the total values and discrepancy are recorded in columns on a spot check form. This form is usually refreshed weekly, with enough columns for each day, however it can be used flexibly using multiple sheets as needed. Recording values sequentially in columns allows managers to compare results and their changes day to day.

These forms are held for a month or so, tucked away in a file, only looked at during investigations and with no long-term records kept.

If you think of an individual spot check being a snapshot in time like a plot on a graph, a group of spot checks together form a picture of changes over time, and the more checks you have in a given period the clearer an image you have of what is going on at any given point.

Of course this assumes you have time to actually look at the data, compare it to other relevant datasets, and make sense of it all.

Change Issuance

Branches will store the majority of their bulk change in their cash office, a highly secure (typically reinforced) part of the building with restricted access to even managers. Our organisation’s policy even states that the branch manager cannot be given independent access to the safe, such is the security of these rooms.

On the tills, bagged coin is held physically closer to the till to which it has been assigned, the digital system having been informed of which till got what. This is typically a very limited amount, enough to cover a single day.

Change Issuance is the process by which bagged coin is assigned to a till from the cash office.

So now we come a little closer to the problem on which this whole project is based, a single question that gets more complex as considerations pile up:

How much change should each till be issued?

And how frequently?

There are so many variables which influence the decision of how much to issue to each till at any given time. The complexity of the interplay between these is the original motivation for the Float Tracker.

First let consider the stakes; Why does it matter? Why do we care? Why not just hold all the cash reserves at the till and keep nothing in the cash office?

If there is too much cash in any given till, it increases the risk of being targeted for theft. This is less about the value of the bags which, relative to the weight, isn’t actually that high, but more about the psychological appeal of a potential thief seeing a large pile of cash and getting an idea in their head.

People can be motivated into stealing by a number of factors, most notably the desperation of the individual and the perceived ease of pulling off said theft. We like to think ourselves moral and “Theifs” and “Criminals” as being a morally compromised other, pre-disposed to evil deeds, but the fact of the matter is that crime is a complicated matter and that any of us, under the right circumstances, can do things we wouldn’t have thought possible.

Risk mitigation factors into the dynamic of how motivations are formed; it sees that once an individual has an idea in their head, the likelihood of pursuing extra-legal activities increases by bounds. It is therefore optimal where possible to stop ‘ideas’ forming to begin with.

While the bagged coin may not be worth much relatively speaking, consisting of a lot of low value denominations, to the right person in the right circumstances, its just a big pile of attractive coin, in a drawer. The weight and bulk of it makes theft ergonomically difficult but this actually doesn’t matter to someone with theft in their mind.

Additionally, having more cash will make the container heavier which will increase the time taken to fill up a till, which very often has to happen during trade when there’s a bustle of customers about. Even if a potential thief figures out that stealing bagged coin is widely impractical, they may still be desperate enough to try and steal it anyway, or they may turn their attention to other aspects of cash processing, wondering where other vulnerabilities lie.

So in short, having too much bagged coin is a very bad idea. The less bagged coin the better.

Having too little bagged coin poses a different obvious risk, that a till will run out of particular denomination during trade. The effect of this happening and the risk factor is, as with everything else, highly variable.

In a basic scenario, a till runs out of a single denomination, and other denominations can be used more heavily to compensate which usually isn’t that big of a deal. However things get more detrimental as more denominations run out. Two 50ps instead of a £1 will not negatively affect a customer to a great degree, but receiving five 20ps or even ten 10ps would be viewed unfavourably by most customers. As some denominations are used more heavily, others will run out faster.

As this situation gets worse, the till may be forced to close which can put real pressure on busier stores at peak times. Alternatively a mid-day cash issuance could be performed but this poses obvious security risks, trailing bags of cash across the shop floor.

So What is The Optimal Amount of Cash to Issue?

As you might expect, the individual denominations do not go down at the same rate. But more than that, the relative rate at which each denomination increases (or decreases) changes based on a range of factors that are difficult to pin down:

  • How busy is the shop?
  • Are there local events on such as a football match which might increase the proportion of cash-users (what proportion of pasangers coming through are on commute and how many are recreational)?
  • How familiar are the local clientele with Self Check Outs?
  • How willing is the local clientele to use card?
  • What is the physical position of the checkout?
  • What is the staff preference concerning this checkout?

Its a very difficult question to answer with no fixed way of deciding, relying largely on the intuition and experiences of a given cash partner to know what worked before and make a judgment.

An Example

One of our tills, numbered 301 is favoured by partners versus 303 because 301 is closer to the door while 303 is tucked away in the corner meaning that in a pinch, 301 is more accessible than 303.

During the deepest Covid lockdowns when we were the only retail unit in the station open, basically only to serve Network Rail and the train crews, the £1 coins would deplete at a vanishing rate on 301, and slightly slower on 303.

As trade increased, this began to slow until the tills would both break even, hardly using their reserves of £1 at all, because the number of customers paying with £1 coins would equal the amount given in change.

Then, as trade continued to increase, 301 would end up depleting its supply of £1 again while 303 would toggle back and forth between staying level and increasing its supply.

If we charted this be behaviour it would look something like this. Not real data. Not to scale. Hopefully obvious.

The Outcome

Brief

Data Driven Actions

During my work as senior cash partner it became quickly apparent that improving cash processing and solving operational issues for staff on the tills was an issue of data visualisation. We knew what the symptoms of the problems were but could not effectively pin down when they were happening, what factors were influencing them, what timescales and processes to investigate, and what to make of the various sources of data we had to work with.

Once a few big easy-target issues had been dealt with and the dust settled a little, patterns emerged which pointed to processes which could be improved.

Its worth re-iterating at this point that malicious theft is only one portion of any problem and usually quite easy to track; practised thieves operate in a pattern, their actions align with a given shift pattern (scammers will target individuals), and they tend to get braver as more time goes on and they believe they’re going unnoticed. In terms of data-visualisation they tend to be quite a “noisy” data stream.

More difficult to track down are the common mistakes made innocently which are far more common, areas where partners do not realise that their actions are causing issues, tracking scam runners and conditions that cause people to rush and make mistakes. This can involve process changes but can also involve talking to people one-on-one, re-training them if needed, while being sensitive to not make the individual feel infantilised or like they’re in trouble. Having a good “case” built and plan thought through is a pre-requisite.

Manual Tracking

Never underestimate the power of a spreadsheet.

The first iterations of the project involved filling a Google Docs spreadsheet with data including the total float levels and discrepancies. The immediate effect was being able to see, for the first time, the long-term effects of new processes & changes and enabled me to convince management to invest the extra time and effort in more frequent checks.

However this required a lot of manual work from entering the data, transforming it, updating the charts, changing the titles, exporting the PDF’s and stitching the PDF’s together in a format that could be used in the end of week report to the managers.

It was also difficult to sample specific date ranges and to jump between time periods because of having to manually change the charts ranges. The whole process is very manual start to end.

Data Clumping & Scale

Spreadsheet data is quite flat; even if you have a large dataset it is difficult to get a sense of scale from a list of entries, compounded by the issue of charts showing one date range needing an awkward update to its settings to change.

With a linear list of data, it is often difficult to get a sense of scale; if you have 6 data points for Monday, one for each other day except for Thursday where you have a cluster of 3, it’s difficult to get a sense of till behaviour at a glance when they’re all just in a list.

Charts generated can, with configuration, display time-adjusted series but this adds time to setup and can sometimes complicate the issue of exploring specific ranges and mixing series’.

I squeezed as much as I could out of the spreadsheet but the more weeks passed, the more tedious the process of imputing the data, updating the relevant charts, exporting the PDF’s and compiling the reports became. The the data itself was stored in the sheet which got to be unwieldy and wasn’t a long term solution.

Overview

I decided that a small but versatile application could not only eliminate the hassle of this multi-step process but could enable users to quickly explore long term data and speed up the process of Random Daily Spot Checks.

The basic brief was to design something to be built quickly and achieve the following aims:

  • To be a central repository to track a branch’s spot check and float level data.
  • To give insights into till behaviour over time and with reference to recent events (like station closures, football matches etc).
  • To allow cash office partners to reduce overall till float values to optimal levels by observing recent behaviour and comparable contexts.
  • To follow a principle of data abundance and the adage “more good data overrides bad data”; users can quickly input counts with no authentication, the occasional mistake or bad count overwritten by more counts and an authorisation process by an authorised user later.
  • To look and feel like any other digital system the staff might use, to share a common visual language and design patterns.

The system should be robust, designed for non-technical users and offering no impediments or blocks in the high-paced, pressurised environment of the tills. When you have to shut down a till and occupy another partner’s time to supervise, you can’t wait for redundant animations to finish, for a lag on every enter-key press, or for the app to hang because the connection dropped.

Count Classifications

The application centres around it’s main data type: Counts. A count represents the result of a single spot check, either a full check or a partial, and can be saved in any state of completion. The server uses a classification system to sort incoming counts and present them in different ways on the front end.

  • Unverified: A count which is submitted with expired authentication or a low-access-privilege user which must be authorised by a higher-privileged user.
  • Incomplete: This is a count which does not have enough data to be included in the data-visualisation areas and analytics sets.
  • Partial: Some counts include sufficient data to render Bagged Coin can be used for some of the data visualisation but not for the full count. Counts with complete Bagged Coins but missing other details are classed as “partial”.
  • Complete: Counts with all values present and submitted under sufficient authentication.

Authentication Levels

To facilitate this data input system, a privilege system was designed to control which actions a given user is able to utilise. For my branch’s use I defined three roles using these privileges, ‘general user’, ‘cash partner’, ‘managers’, but in theory any number of users could be defined, one per person if you liked.

  • Standard User: This role has an empty string for a password that only has basic write abilities; counts submitted are saved as “unauthorised” (which really means ‘unverified’) requiring a cash partner or above to approve them. The lack of authentication is a strategic choice based on evaluation of how much damage a malicious user could do.
  • Cash Partners: This role is for any trusted partner, someone who regularly performs spot checks. This role allows the submission of fully authorised counts but still cannot remove data, only mark it for review.
  • Managers: Includes senior cash partners and management who oversee cash. This role has all write, update and delete capabilities for all data types. This role has a much longer password and quick session timeouts.

Design Prototyping with Figma

Learning from a couple of projects which preceded this one, I was adamant that this project would be designed upfront with prototyping software, fully thought out and with strict adherence to brand design given it’s context within the shop.

My previous projects immediately before this one involved building a clone of an old Microsoft Windows game in which I focused on building the engine and algorithmic structure first, thinking about its layout, interface options, look and feel, use context and other details as I went along. After all, the engine and mechanics were the thing.

This wasn’t necessarily a bad ideas but as time went on this lack of direction towards a single, clear end result (beyond replicating the main functionality of the game it was based on) created problems given that some of this functionality is tied to, or influenced by, the desired look & feel. Continually stopping a train of algorithmic work to come up with an interface idea which will potentially be rehashed later is an impediment that slows progress a lot.

Figma is a fantastic tool for design and prototyping, I had already used it for numerous pieces of work including Tailored Nutrition, Mosaic, lots of client work, and the redesign of this site. It allows you to pour thoughts out onto a page like a digital scrapbook but also to create mock-ups which look identical to the equivalent HTML and CSS. Its fast and versatile with a very low learning curve so check it out if you haven’t already.

Brand Analysis

I began by collecting material relating to the branding of Waitrose & Partners, first looking at the official brand guidelines, core colours, and examples of where these are used in existing digital sources such as their websites and internal applications (not pictured here for security reasons).

Beyond the core colours I wanted to look at the other pieces of tertiary branding such as the “Give a Little Love” campaign which was in full swing at the time, and other commonly used colours. I was particularly interested in how white-space was filled and how contrast is used.

It was important to me that the app feel a part of the eco-system, that it read to partners as a Waitrose app but I didn’t need to stick to the brand too exactingly; other sources take liberty with specific colours.

I had a rough idea of the layout that I wanted, it had to focus attention on the man content but also allow the user to jump between areas quickly so I decided on a side-bar based navigation layout with page titles and page-specific details on a bar running along the top.

These decisions were decided in response to one of the main applications I dealt with in my cash-office duties. This application, which we will call Flint here, presented users with an empty page and left-aligned top-level menus. On click, these presented cascading drop-down menus to get to the individual section you needed. This section would then open up in a new window, meaning that the main page wasted 90% of its space while still requiring the user to navigate tricky cascading menus.

I knew that the app would involve a fair amount of lists and large vertical input forms so a side-bar layout eliminated the need to handle a top-level navigation.

It was imperative that the application stack and / or hide its complexity behind intelligent design as much as possible, it had to focus the user on one “thing”. As discussed in the context section, most of the users are non-technical and the use-case environment may be high paced so distraction, confusion & clutter is completely unacceptable.

The diagram bellow shows an approximation of how Flint’s main page looked, although in reality the menus are about half the size shown here making them difficult for even able-bodied users to interact with.

Bellow is a hypothetical refactor of Flint with the exact same content. A clear separation of concerns focuses the user on the main page content, tabs are used to keep everything on show at once but these could easily be cascading menus or even a drop down / sliding drawer, just so long as the content is larger.

Not only is space used more efficiently but large amounts of overflowing content can ba handled far more effectively than before. This basic layout shows what the original design for the Float Tracker based on.

I then iterated some layouts and colour combinations to settle on the look and feel of the application.

After deciding on the layout system, I designed the main pages and user-interactions. I wanted the user to land on a list of graphs to communicate recent levels across all repositories and then click into each graph to show the inspector view for that repository. We also needed a central list of counts which should be filterable, and a reusable component for entering a new count and editing an existing count.

On top of these there would also be pages for managing the data types; users, repositories and partners. A User is local to the application and controls authentication and authorisation while Partner represents the till login number and name of am actual person and is used to sign counts. These pages were low priority given that the application could run with manually updated seed data.

A persistent “Add New” button was added to most screens to allow users to quickly jump into a new count irregardless of the current state of the app, the button was placed and styled to create an association with a common design pattern found on platforms like Twitter and Tumblr which have similar features.

Modals were selectively used for many Create Update and Destroy (CRUD) operations in order to keep the user ‘in place’ and to help with component reuse. With actions like deleting a count, a reason must be recorded and the user must identify themselves by selecting their till number, creating a compounding complexity of forms on top of forms so modal pop-ups are used to simplify these interactions and to bring the user back to their starting point on exit.

Theme and Component Design

Once I had hashed out the layouts and user-flow, I gave a pass over each frame to ensure consistency between colours combinations, text sizes, margins, drop-shadows and so on. From this it was possible to define named colours and a set of “basics” colours which replace the native browser named colours like “red” and “green”.

Another feature I wanted to try for the first time was the implementation of a “dark mode” and “light mode” theme, up until now all of my applications picked one style and pretty much stuck to it.

I took my primary inspiration from GitHub’s dark mode which appealed to me with it’s use of neon colours; at the time I was still not much of a fan of dark modes. One of my main issues with the way that many applications / sites implement dark mode is that often there are contrast issues, the contrast levels either being too low and difficult to see, or detailed elements like text being too bright and sharp, straining the eyes. Sites like GitHub that get it right largely do so because they solve this issue in particular.

I was keen to find areas where the same colours could be used for both light and dark modes such as the gray used to outlines and borders on inputs, the red warning text, some buttons etc.

I collected the common elements into one place and added other components which might be used later and collated them into a component library.

Tech Stack

The base of the font end was a React-Redux application for performance and data sharing between different elements of the state. React context was used heavily for local shared state for things like count editing and the inspector views. The application is a single page app using React-Router’s hash router given it’s ability to port to React Native if that option became desired later.

This project was also my first big typescript project, using the typescript create-react-app template for the front-end and a custom typescript setup for the back-end.

The back end is an express app secured with passport.js for authentication. There was no hard plan for the application to be used outside my branch however the idea that other branches could hypothetically spin up their own instance was important and so there was a plan to dockerise the app.

The app used a component and CSS-in-JS framework, unusual for me, with Emotion and Chakra. I was not amused by it. See the later sections for my thoughts.

For the back end I was keen that to use a SQL database, moving away from MongoDB; I was happy with my Mongo knowledge and realised I was lacking practice with SQL which is still more flexible and widely used in industry. I designed the data tables in Lucidchart’s diagramming tool with the entity relationship package, which you can view here.

To build the database, handle migrations, and build SQL queries Knex.js excels, allowing you to chain functions together representing the components of a SQL query in nice familiar JavaScript. Objection.js is an ORM (object relational modeller) is built on-top of Knex and allows you to build models and define database entities to interact with your database. It provides a comfortable middle ground / bridge for developers more used to things like MongoDB.

Chakra & Emotion

This could also be an entire post in itself, and may be some day.

This project was an opportunity for me to push myself to use new technologies and deviate from my typical tech stack. One such area I had previously been staunchly against was the idea of using a component library framework. I’ve used component libraries in the past but only for specific needs like date-pickers, the majority of the components would be custom made and the main layouts and styles implemented with CSS / SASS.

I was inspired to use Chakra.js, a component library built on-top of Emotion.js, one of the most popular CSS-in-JS libraries.

Chakra lets you pass props representing CSS attributes to your components and is touted to be a convenient way of quickly writing CSS. I found it to be a somehow even worse implementation of the already misguided utility-class method exemplified by Tailwind. This is definitely getting its own article at some point.

However this alone is not enough for me to not give Chakra a fair shake, what killed the experience for me was that a) Chakra is inconsistent on how controlled components (like text inputs) handle change events. Never before have I spent as much time fighting with an input to behave in the way I expect or trying to figure out if I should use <NumberInput /> or <NumberInputField /> .

The second thing was b) There was no date-picker. What’s a component library without a date-picker? Jokes aside, Chakra will frequently lack components that you would expect to be featured by default, and are the reason you use a component library in the first place.

You’ll be getting along fine, using <Box /> after <Box /> wondering what the functional difference is between just writing a regular <div /> and writing some CSS like a normal person, and then Boom! Stopped in your tracks and searching through another article listing component libraries which will need to be stylised and add bloat to your package size or through inscrutable documentation.

You become a designer for the framework, focusing on it rather than benefiting from it working for you.

React Vis

There are a number of chart options when it comes to a React application, I won’t go through a full breakdown as that could also be a post in itself.

D3 is still my preferred option for data visualisation but it is tricky to use alongside React given that it works by injecting data into the DOM and performing transformations progressively to said data. This poses a problem given that, strictly speaking, any manual DOM manipulation within the React app is an anti-pattern.

If you just search for “react d3” you’ll find many articles and an unmaintained ‘React-D3’ library which attempt to integrate D3 with react. Almost all of these will make the mistake of simply placing some amount of D3 calculation logic in useEffect’s and then simply placing the output in the render method, violating the basic benefit of using React and frequently causing unnecessary re-renders.

Some libraries such as chart.js calculate the output of graphs behind the scenes and then render the output to a canvas element. This eliminates the DOM manipulation issue as the library can track changes and is effectively providing the framework with an image to paint on each render.

The drawback is that you loose out on the flexibility of D3 and are locked into preset graph options and whichever configuration settings your chosen library supports.

Then there are libraries like React-Vis which lay somewhere in-between; they provide wrappers for pieces of D3 functionality that are designed to be used in a compossible manor.

React Vis was made by Uber and was the library I chose to go with after research as I felt it was the best all-round compromise. Its compossible components are rendered intuitively in the JSX and can be configured by passing props, which I feel is much more in keeping with the style of writing React components than calculating and building a configuration object and passing it to a single <Chart /> component, but this is of course personal preference.

Good chart libraries usually already provide combination charts but what’s nice about the React Vis method is the ability to simply bring in a LineSeries or PointSeries and place it in the JSX. Want to get rid of the Y-axis? Just delete it! No need to scour the docs to find the right option to turn it off.

This is still a compromise, far from perfect, and I would still consider all available options per project but until someone can properly integrate React & D3, a good compromise is the best you can try for.

React Context and Reducer Hooks

This type of application has a number of areas where a large amount of shared state is used by several components but is still too localised to be put in the global Redux state, the app can be thought of as a collection of mini localised unit-applications that communicate with one another. The two most prominent examples of this are the count edit / add form and the inspection viewer.

With this type of situation the temptation would be to hold all the state logic in a top-level component and pass the necessary data and callbacks to each child, which can then pass them down as needed. This introduces a number of issues and inconveniences not least that these types of prop-drilling and callback trees are the problem that Redux is supposed to solve.

In addition, when child components need to perform data computation and complex algorithmic work, the top level component becomes bloated, loading and perform data parsing at inconvenient times and when the consuming component is unmounted.

By combining the React context and the hook useReducer we can create a Redux-like experience without exposing unnecessary data like mouse positions, series highlights and tooltips to the Redux store unnecessarily, and does so in such a way which compliments typescript.

useReducer allows us to dispatch actions to a reducer, just like with Redux, and have the reducers make immutable changes to a state object. Placed within context, each component can access the state object and access the sections it needs and dispatch modifications as needed.

By using enums for the action types, and action creator functions, enforcing type conformity is easy. By extracting the reducer, initial state, action creators and action types to their own Utils file you can create a separation of concerns and import what you need in each component.

Notification System

The application has a notification system built in, initially intended to provide the user visual feedback on a save action.

The implementation is fairly simple; an invisible layout is rendered over every page, it subscribes to a list of queued notifications in the Redux state. Each notification is represented by a standard object with options that specify how the item should render and a time property which, unless the notification is specified to be persistent, will define the amount of time before the notification self-dismisses.

Some notifications have links or actions which will dispatch events to change the state of the application (like redirecting the user to the inspector view with preset options). There was provision to have some notifications stash themselves in a drawer for the user to view again, similar to how many operating systems implement notifications.

The advantage of this system is that the notification can contain anything, it can render with a particular colour scheme such as a blue “information” for external events that the user may want to be aware of, a green “confirmation” for things like confirming a successful save, and a red “warning” for negative side effects like a network error.

This means that anywhere in the app I can write a simple dispatch function, pass in the required and optional props and have a notification appear. Notifications slide in from the side on top of any which are currently rendered, as others are dismissed the stack drops down. When the user mouses over a notification the expiry timer is paused.

Inspection view

While the main landing page graphs provide a brief comparative view at a glance, it is the Inspector view that provides users the ability to pull the majority of insights.

The inspector view allows users to quickly jump through date ranges to get a snapshot of the tills state and judge it’s behaviour but also to drill into specific counts and compare the change over time.

Date ranges

The date ranges allow users to filter the queried data and regenerate the graph seamlessly, with a minimal load time. It defaults to showing the last four weeks of counts but can theoretically show any time frame, given that the application has no gauge of how many counts exist in a given stretch.

Data Inspector and Series Selection

Two tooltips allow the user to interact with the graph, the first showing overall details for each count such as the date and float total (although the version in these screenshots here only show the timestamp).

The ‘local’ tooltip shows data for the specific data point and can be clicked to highlight the series to make it stand out.

The information is already distinguishable from other elements on the graph including the time series, the height of the point, the series colour guide etc. The the tooltips allow users to focus their attention and explore from point-to-point, without having to jump back to the series labels. This helps simplify what can be a complex array of data.

Series Adjustments

The inspector has two modes, one which shows just the Bagged Coin and another which merges Bagged Coin and Loose Coin by calculating the effective amounts.

Bagged coin has the disadvantage of being uniform in value; you’re never going to have more than say 10 bags, the bagged coin will usually be somewhere between 3 and 6 bags. This poses the problem of overlapping points and series, which are difficult to read.

The “adjustment” feature calculates the relative amount of each value and changes them to offset by a set amount. Each series will receive an “offset”, a value which it uses to change the vertical placement of each point, the series are then clearly visible as being in the same position but distinct.

Of course, the series could swap positions every step between counts creating a “leap frog” effect, but chances are such that a given series will remain above / bellow another for a period of time.

The following examples show situations in which an ‘adjustment’ is useful, and where it can conflate issues (see the left side in the third image, the series begin to look as though they are on the next line). These images show the same datasets, the first has no offset, the next has a small offset, the last has a large offset.

The series algorithm will alternate between offsetting positive values and negative values, for example, the first item will be offset by +5%, the next by -5%, the next by +10%, the next by -10%.

Shadow Points

Given that there will be between 8 and 20 values, it makes sense that the series lines and points would be small, this poses an issue given that small points are difficult to focus on with a cursor.

The app renders “shadow points” behind every actual point which provides a larger target area for pointer events, meaning that its easier for users to hover and see specific details on the tooltip.

Count Input

The component used to edit and add counts is the other heavily used section of the app. The same large component makes up the majority of the page and so the distinction between the ‘edit’ page and the ‘add new’ page is whether data is pre-loaded into the state.

In fact, when adding a count, the first save will redirect the user to the edit page with minimal perceptible effect to the user.

Count Validations

The validation functionality is used to perform two main functions, certifying that a count has enough data to be displayed alongside other counts, and to categorise the count into the completion states; “incomplete”, “partial”, “complete” and “unverified”.

Both the front end and the back end use a validation function, and unfortunately for me that meant keeping the two in sync manually, copying changes back and forth depending on what changes I needed to make. The front end uses the validator function to warn the user of the current state of the count and to attempt to reduce re-saving.

The back end is more strict and must use the validation to change how it saves each item (or perhaps not, should it fail certain checks).

Number Steps

The input components for bagged coin, loose coin and notes are all reusable, they normalise values into their pence value given that 1p is the smallest possible denomination. All calculations are performed on the pence value and validated by calculating valid “steps”, e.g. a 20p input will go into an error state if it is given the value “£0.30”.

The first image shows how the reuse of the single input for a loose coin series, the second shows the logic used to add trailing zeros to the display value when the user tabs or clicks away.

User Selector

By the rules of the partnership, each count should be performed by at least two people, the ‘counter’ who performs the count, and the ‘supervisor’ who observes the count without interacting (they may write down the values or help out in another way).

The input allows users to select their name from a drop down list or type to filter the results. This matches not only the user’s first and second names but also their till number and initials meaning that however the user tries to identify themselves the system will try to approximate who they are. This is helped by the fact that there are a limited number of users per branch so the result matching can be quite loose.

The user can then click from the drop down or hit enter to select the top result.

Submission Panel

The form presents users with three buttons to submit the count, “Save”, “Save and Close” and “Save and Next Count”. This form is set to a sticky positioning at the bottom of the page meaning that it will display at the end of the form if the form’s end is on screen, otherwise it will pin itself to the bottom of the screen.

“Save” saves the count and displays a notification while allowing the user to continue entering data, “Save and Close” does the same and re-directs the user to the list of counts or back to the page they were on (only available for some locations), “Save and Next Count” saves and redirects to a new count.

In the Real World

You’ll notice if you view the app that its a bit rough around the edges and even missing some sections, not least because the branch on deployment is behind the main branch due to embedded sensitive data that I don’t want to put on GitHub.

Perfectionism was a real impediment working on this project, it was never finished and I was admittedly shy to show it to my colleagues before it was “done”. Ignoring that it will never be ‘done’. I started using it in March of 2021 and kept it mostly to myself at first.

I cant remember what prompted me to show my colleagues but I’m glad it happened so soon, the best antidote for perfectionism, and the assumptions and lies it plants in your head, is to expose it to other people. My colleagues were impressed and eager to get their hands on the app.

We never got to the stage of actually imputing numbers on location at the tills during a count, but we were able to input the data into the shonky, half-built graphs, and pull a number of key insights despite it’s half finished state.

The app made the end of week reporting so much faster for me and was used to track down a new scam which had just cropped up and was evading us at the time.

It allowed a massive reduction in float sizes and for us to find ways to build resilience into each till preceding events which were happening around that time. This included the run up to 17th July 2021, the day in which all Covid protections, including basic things like mask-wearing in indoor public places, were to be prematurely removed in England (but not the whole UK). This was a period of rising trade, beyond pre-covid levels, and further uncertainty about what the developing behaviour from the public would be as we moved into the next phase of the pandemic.

By the time I left the partnership in September 2021, the tills had held steady at a discrepancy of +-£10, the “very good” band of operations, something we had all assumed was a goal achievable by any branch except ours, an exceptional outcome for the project.

Still a little bummed I didn’t get my consistent +-£5 though.

Reflections

App professionalism comes at a time cost

As pleased as I am now by the way this project turned out, I cannot deny that it became a monster incarnation of something which in all rights should have been smaller and faster to produce.

I felt that this project took an inordinate amount of effort considering the relative simplicity of the outcome; “how come this app is not really more complex than my past projects but took longer?”.

Well, one explanation is that being an essential worker during a time of global crisis an having to interact with costumers every day who seem to flip-flop week-to-week between treating you as “the hero’s keeping the work running” and “just shelve stackers who are not bringing me service fast enough, Covid is over now don’t you know?” has a detrimental effect on one’s mental health and creates burnout.

Another explanation is that this application brought a level of production maturity that pervious works such as the chess app just did not have. A project like this had to be a showcase of skills built up until this point and deigned as though it were a production app to be maintained by the organisation after I left.

Many of my projects until now had had a focus on a particular core aspect like algorithms, a game engine, CMS functionality, etc. This one had to do everything from brand identity to a user and consumer focus, to new tech stack changes, to security concerns and so on.

In retrospect, a lighter, more stripped down version of the app could have been produced faster to begin generating insights long before it got to its current state.

Typescript should be top-down

My next project, following on from immediately from this one, was the Scratch Card Tracker app and re-used much of the tech stack from this project, with a number of improvements.

Given that this project was my first big typescript project, the most noticeable improvement in the Scratch Card Tracker was the quality of typescript used.

The Float Tracker suffered from inconsistent use of typescript standards with many instances where TS wasn’t used or where there was an abundance of any and unknown declarations to get the project moving again. This is somewhat to be expected from people new to typescript or new to strongly typed languages entirely. Unfortunately it creates a feedback loop as the more weakly typed a section of code is, the more brittle connected sections of code become, and the more you feel the need to just drop an any and move on.

Starting The Scratch Card Tracker I defined data-types from the beginning such as Actions, State types, ColourSets, Counts, Games (series), and stored them in a central file (although I must acknowledge that there are opinions about how to handle global typedefs but who cares frankly given how closely tied all the functionality of the app is, I saw no reason to not just import them from a types.d.ts file.

Having core data types right from the beginning meant that sub-components inferred stronger types (where TS “figures out” the type of a variable from its source) and so made more strongly-typed bindings, meaning the incentive to use ‘cheats’ like any were reduced. Strong typing cascaded from the top of the app downwards instead of having to be retroactively applied by adding them lower down and following the error stack slowly upwards.

This is not to say that having set data types is the answer to all the woes you might encounter with typescript, and there are certainly times when it is useful to just remove type definitions, but it’s worth thinking about areas where you can introduce strong type definitions from the beginning and cascade them downwards rather than getting caught out later on in the development process.

As a general rule, if your weak type bindings are lower in the component tree / call stack then you can probably get away with it. If there are weak type bindings at the top but constantly strong bindings later on then you’ll typically be alright but may run into some issues. If the type binding is chequered or has an area of weak bindings ‘half way’ all hell breaks loose. Lastly, if you want strong type bindings later on but none higher up you might as well walk away and go a bike ride or do some painting instead and never touch the project again.

This diagram communicates my general thoughts on type-bindings; consistency is key, and it is easier to put strong typing up front and become loose later than to try to introduce strong typings later.

Proper prototyping is essential but don’t get bogged down

Beginning this project I had a good idea of the necessity of prototyping, but I think I had slightly inflated ideas about how absolute a design could / should be before actual work can begin.

In an ideal world it would be possible to design every component, every component combination, every colour, every modal and every page / state and so on. In reality, not only is it not worth your time to mock every single modal, so long as every base piece can be found elsewhere and the general user flow is thought through.

In addition, there will inevitably be changes that come up through the built and UAT feedback process, its just what happens. You’ll find yourself going back and retroactively adding features to the design from the real deployment and diverging from the design.

Going forward I considered the prototyping phase to be essential but also sought to view it as a starting point for the project, to eventually be jettisoned and to grow beyond, not a strict rulebook to be bound to.

Stop trying to find a way around it and just write the CSS

Alright, I admit it, I am biased against component frameworks.

However I do still maintain that there is a right way to build a component library / framework and there are about 12 wrong ways. Perhaps a framework is inherently a compromise, that there will never be a “perfect” one because each will invariably have to choose from a set of trade-offs.

I’ve since worked on teams using Material UI, an implementation of Google’s Material Design system which has a range of it’s own problems but is rarely as blocking as Chakra was to work with.

MUI has a wide-ranging set of components out the box, very often we were surprised at how often a desired feature was present. It’s various methods of writing CSS-in-JS are still sometimes a pain in the backside, but I do appreciate that they’re trying to cater to a wide range of audiences and seem to do so well.

Ultimately though the source of much of my frustration comes from the fact that much of these technologies seem to be convoluted 5-dimensional-chess manoeuvrers to avoid just writing some CSS.

I understand the drive to encapsulate pieces of styling instead of having enumerate SASS files compiling into a global CSS sheet which then causes unexpected conflicts between components but, for fear of sounding like a stereotypical pensioner whining about “back in my day”, I can’t help but feel the solution is to just write better CSS?

We ended up having to define quite strict standards for setting up the ‘theme’ in our projects in order to try to standardise styles between components, and encountered situations where two unrelated components should look alike but diverge slightly, because at some point a chunk of CSS was copied and pasted and then unknowingly conflicted with something else in the page. Encapsulation moved from being the thing that enforces consistency to an impediment to consistency.

Every time I find myself tracking one of these issues down I cannot help but think to myself “what if this was just a SASS variable, wouldn’t that get rid of all this? Is this not a solved problem already?”

Anyway… get of my lawn you darn kids! And take yer no-good gadgets and googas wicha! I got my SASS runner and I like it! *harumph noise*

Trying to prove a point with code.

There is no project which exists outside of a context and a human story and this was no different, there’s also a reason I’ve struggled to get round to writing it until now.

I’ve joked at several points about the mental and physical stain and burnout of being an essential worker, in a retail unit, in a station, across the Covid pandemic (at least until I left in September 2021). Putting the jokes aside it has had long lasting effects and I’m not sure any of my colleagues or myself are quite the same people we were before it all started.

At least through much of 2020 we had the sense of urgency and emergency which stood to validate the stress of the work; it was tough but we had a duty to perform and were up to the task. By 2021 we were all severely burned out and it was showing, the camaraderie and “we’re all in this together” of the past was gone and the continual flip-flop between people taking Covid seriously and acting as though it was all over (and definitely not coming back, this definitely isn’t the 4th or 5th time we’ve said this) was compounding the issue.

We can’t disconnect ourselves form the broader context from our work and it is clear to me now that this project, and the ones to follow were a way of dealing with the trauma, frankly speaking. I could work on something which gave me a sense of achievement, something to focus on rather than the outside world, and something which I believed was a logical step up to the next phase of life, Covid or no Covid.

I believed in the partnerships mission and had the idea that I could use this and the Scratch Card tracker to get my foot in the door at the partnerships head office, driven in part by the belief that I was not qualified enough to just search for a development job outright (a strong case of imposter syndrome).

This would also helped create a bit of narrative consistency; I would use my position in the partnership to improve the branch, leave behind a lasting legacy, and then move into a developer job within the same organisation which could not have been obtained otherwise. This, it was felt, would justify the hard work, the sometimes back breaking effort, and the lack of any meaningful break in over a year, and ‘make it all make sense’.

In reality, while my skills were benefited by the project greatly, this project and the Scratch Card tracker were not even on my portfolio when I applied to my current position, and the partnerships head office rejected my application outright; they needed the team leading experience more than the specific skill-sets.

I was so deep in my own little world, head down, just trying to get through each week that there was no time to stop, take a breath, and think rationally.

The thing that eventually ‘snapped me out of it’ was my living situation falling apart; my then flatmates turned abusive and threatening before trying to kick me out and I had to pack up and move back to Glasgow to bunk with my parents. I was out of options, and probably on the verge of a nervous breakdown (yeah that was a fun couple of years, I love London really…).

It feels as though every one of my posts in recent years has ended with a section about burn out, but this had an air of finality about it. The reason I was so engrossed in this uni-linear career / escape plan / escapism, and the reason I was so attached to the partnership was because it created that narrative consistency; it meant I was working towards something when, in reality, I had all that I needed to get my current position from about mid-2020 onwards.

In retrospect it is easy to say that we all (myself and my team) did good work irregardless of context and irregardless of appreciation, that the time spent making these improvements and pushing each other was not wasted, and has had lasting effects but that thought doesn’t help when you’re in the midst of it.

Still, if there is anything in this which you think resonates with you or someone you know, take this as the ‘red flag’ sign you’ve been waiting for.

Mosaic

When I first started out in design, my only motivation was to contribute something, to ‘save the world’. For my final year project I forewent personal comfort to platform an issue that affects so many of my community on a deep personal level.

Mosaic Voice is a consumer-viable Electroglottograph (EGG) designed to help transgender people (specifically trans women) perform voice therapy training.

Comprising of two parts, a wearable EGG, and a supporting app, Mosaic was conceptualised as an extensible system, providing the basic software, while region and use-case specific needs could be met via a library of plug-ins.

This was based on first hand experience and extensive research which indicated that any solution would be region-specific and require outside experience for each subsequent location / context it was to be applied to.

Voice Therapy Training

Voice therapy training is a sensitive and highly personal process which poses significant practical difficulty and emotional strain for trans people with vocal dysphoria. Resources are scarce and sparse; embarrassment pervades around stigma about adults performing vocal training.

Democratised design can facilitate this process by creating assistive tools and means to alleviate the emotional difficulties incurred.

Voice therapy training (VTT) is a broad term for practices and training regimes designed to modify the voice to take on a different appearance. It can be utilised by singers and actors but also by people recovering from injury and trans people, in particular trans women such as myself, who’s voices are not affected by hormone replacement surgery.

Issues with Specialist Help

Specialist tutors & therapists exist to help with this process, offering guidance, techniques, coaching and so on, usually over a long period of time. I personally make use of the excellent service by Christella Antoni, who takes a holistic approach to sessions, integrating social aspects and involving the client in the process. It should be noted that I am not affiliated with Christella Voice, my opinions are my own and genuine.

This works well to tackle the individualised nature of voice and offer professional guidance, the issues arrise with the fact that so few people actually perform this service. People travel from across the country to attend sessions because there’s nowhere closer, the cost of travel tickets exceeding the sessions. And even then, one person can only see so many clients.

There’s also the issue that many trans people are, for lack of a better term, broke.

Trans people are massively discriminated against in work, promotions, housing and healthcare, among other things. This leads to a significant majority living bellow the poverty line. This works to make access to services like Christella’s difficult for some to manage, then additionally, you must consider the viscous cycle that one’s ability to “pass” is largely dependant on voice, which has a compounding effect on discrimination faced, which leads to more poverty, and so on.

This leads to many of us (if not most at some point), turning to the idea of doing it yourself…

Issues with DIY

Many people turn to resources on the internet such as YouTube videos, the few training apps which exist, and the occasional Reddit post which is cited by everyone you know and threatens to dispensary one day. Also you have to tolerate looking at Reddit.

The issue boils down to the fact that, with say a YouTube video, you are seeing a “patient’s” personal reflection on what they deem to be the most memorable aspects of a highly personal journey, as opposed to structured content.

Even when structured content is available, it is often nullified by the lack of tutoring context and practice structure. There is an issue of “hearing yourself”, that is, gauging correctly where you’re progress stands, what you should be working on still, and knowing when to celebrate achievement.

Three Target Issues

This lead me to define three topic areas to explore as the basis for my dissertation

  • Visualisation: concerning the issue of self-reflection, issues around hearing yourself and your progress
  • Tool: A set of techniques and resources, not to supplant specialist support, but to aid in self-practice
  • Goals: Looking at the auto-therapeutic aspect of VTT, aiming to address the physiological distress and discomfort as well as help people define obtainable targets.

Electroglottographs and Self-‘Visualisation’

The ‘desktop’ version of the Larynograph which I have had experience with

One of the most useful physical tools employed by voice therapists is an Electroglottograph, a device which provides in-depth data on the behaviour of the larynx and audio aspects which comprise the voice.

These devices are difficult to come by, large, clunky and expensive, hence few people who are not specialists are likely to own one.

Their function is effectively quite simple, an electrostatic field is created across the contact probes which are pressed into the client’s neck. The various vibrations and noises interupt the field across various channels, which are then picked up by the device, and processed into usable data by the software.

Rethinking the EGG

A Precedent

Initially, I assumed that designing a new EGG was impossible, so I pushed the idea away. In early December (just as everything was winding down for the winter break), I came across two articles concerning the creating of DIY EGG devices.

I am research and engineering driven in my approach; I was not willing to create a project based on the assumption that a new type of EGG could be made without first seeing something to indicate it’s validity, and then packaging this in some way into a proof-of-viability / proof-of-concept.

The first resource I came across was this project by Marek Materzok on Hackaday.io which documented their process of making and refining an EGG device from scratch. There was not enough information to replicate the process but it offered a beginning insight into some of the challenges such a device would face (namely noise filtering and the best way to create the oscillation).

This lead me to this tutorial on Instructables of all places, DIY EEG (and ECG) Circuit by user Cah6 which gave details and specifications for building an Electroglottograph from simple components. This was all I needed to tell me that it could be done.

I made inroads into building my own version but decided to allocate my time elsewhere given how late on in the project I was.

I decided on a wearable typology given the ergonomic difficulty encountered with the strap-on probes. The device would hook round the users neck, designed to cradle on the shoulders. The hinge components on each arm flex to adjust for neck sizes while retaining points of tension on the probes.

Most of the circuitry and interface buttons were placed on the back, with the batteries closer to the shoulder blades, this was to achieve weight balance on the arms but also ensure that any imbalance would only serve to pull the probes against the neck more, not slide off.

Development sketches for the wearable
a diagram of the wearable
The device stretching to fit a larger neck.

Chips on Both Sides

Using Cah6’s article as my template, I found an optimal size for the EEG control chip, which would be placed under another board which would handle interfacing with the ports and network. This second chip was designed around the Broadcom BCM 2835 controller used on the older Raspberry Pi’s given the low cost, versatility and proven record it provided.

Other smaller components such as the WiFi chip were also taken from the Pi series. Cost was a primary motivator with most of the deisgn descisions, given that this device had to be as low cost as possible.

render of the CAD model
Two images showing the interface PCB on top of the EGG

The chassis is comprised of simple injection moulded nylon and designed to be easy to disassemble, repair, hack, etc. Screws are standard size and not hidden, components are all accessible, the batteries are lithium-ion AAA’s so can be swapped out at any time.

Motivation and Repetition

For the software end, I layout out a feature map for a mobile app including:

  • A modular daily training system: Inspired by the Enki app, this would show ‘Pathways’ which would utilise the following tools to guide users though speaking exercises.
  • A set of quick practice tools: These would show a simple animation or instruction and allow the user, in bursts of 30 seconds or so, to practice some aspect of breathing or warm-up.
  • A pitch sample recorder: An area to record and sample the voice over a piece of sample text to view pitch over time.
  • A resonance estimator (using neural networks): While the EGG is needed for accurate resonance sampling, this would provide a middle ground for people without financial access. Using a pre-trained convolutional network, an ‘estimation’ of resonance levels could be pooled. This would record the samples in the same area as the pitch sample recorder.
  • A continuous listening sampler: Somewhat experimental, this functionality would note samples throughout the day of the user’s voice as they perform their daily activities. This could be used by the user to see how they remember their training in various, uncontrolled environments.
  • A voice pattern matcher: Would depend on finding the right region-specific data set. Another convolutional network would match the user’s voice with one that sounded similar in most respects but could be adjusted for vocal features. This could then be used to practice against and set goals for the user to aim for.
  • A voice creator (neural networks): Would depend on finding the right region-specific data set, a recursive generator neural network would modify the input voice to be adjusted for vocal features such as softness, tone, pitch, resonance, etc. This would allow the user to, for example “gender swap” their voice to try it out.

I built the frame for a progressive web app to demo these features which I could implement now, and provide dummy data for items that would require live data.

Nuemorphism

At the time (late 2019), there was speculation rising about the concept of “Nuemorphism”, coined as a play on “skeuomorphism” by Devanta Ebison. I hadn’t made up my mind about the style but I saw potential for a textureful and soft, welcoming interface which would be great to try for the app.

The result was a pleasing warm aesthetic, I especially liked such items as the progress indicators which felt like little gems that you wanted to have, empty ‘slots’ for the unfilled sections.

I’d like to write extended thoughts about the topic at some time, but sufice to say, while I liked the unique aesthetic of this app, it only worked due to the colour contrasts and would have faltered slightly if I had followed the pattern where a ‘raised’ section is the same colour as the background.

This is one of the critical flaws with nuemorphism (and skeuomorphism to a lesser degree), it’s smooth transitions and drop-shadow facilitated layer / element separation are often incredibly low contrast. This is a problem on displays with lower contrast settings or fewer colour bands, items viewed at any sort of distance, and of course, accessibility.

The advantage of more Matrial-UI esque drop shadow element-separation is that you can still use other features such as subtle borders to add definition and get round this issue. Even skeuomorphism (which, for the record, I am not a fan of), relies on heavy gradients and colour mixing to get it’s textured effect.

Reflections

The project concluded successful, I got an A and the presentation was received well. Its one of my favourite pieces of work and I’m proud to have it as my final year project.

But I still have hang ups.

This was, for all intents The Big One™, the final year project, and more than that, it was something I so closely believed in. It wasn’t enough for it to be good or even great, it had to be a master-piece.

I was afraid my work couldn’t speak for itself

I kept diversifying the system while not building on what was there. It’s true that the solution should take the form of an integrated system but I remember over and over again not being sattisfied with what I had, constantly striving for something to truley step up to the next level.

In reality, I was having a crisis of design, I saw myself perched between two worlds, one of Product Design, and the other of Engineering / Code. I told myself over and over that there was no divide, that we all have ranges of skillsets but I couldn’t chake the feeling that I was a jack of all trades and a master of none.

So I kept adding ‘stuff’, imagining the basics of a complex system and then trying to work back from there (the egineering approach when you actually know what that system is). I spent a month on a little breathing excersiser device before stepping back to ask “what the * am I doing? What is this?”

When I shifted hard into the EGG route, I was doing so over the break, working tirelessly while others were relaxing, just to catch up.

I used my specialisation as justification for not re-evaluating

Perhaps what is worse is that the warning signs were there; clear indicators that I should clear my head, define one or two things that the product had to do, and just worked on those.

I let myself believe (not incorrectly) that I was uniquely positioned to pull off engineered solutions comepletly unlike anything my collegues could do, due to my specific code based skills. This is a mistake that would unfortunately not fully reveal itself until Tailored Nutrition.

In doing so, I chased these mutliple vauge threads instead of simply doubling down on the core that ended up being the final outcome. I ended up working twice as hard as some but still “only” producing what I could have under older circumstances.

I assumed that I could just grind to finish

Perhaps the most egregious mistake I made whilst all this was going on was the assumption alluded to, that I would miraculously pull off the feat at the last minuite. As I said, I did do that, but for an outcome which could have happened regardless.

I made a habbit of being in the studio from 10:00 until 23:00, calculating when would be the most ‘efficient’ time to drink caffinated drinks, pushing msyself beyond physical limits. I could have halved that time, used the remaining to rest, research, regain my humanity, but instead I saw time as a commodity to be collected and horded as much as possible.

We all have pressing deadlines from time-time. But if you begin to see time itself as an enemy, its overdue you stop and reevaluate.

PinApathy

A Pinterest.com clone built with React on an Express back-end. Authentication by Passport.js using both local and 3rd party oath2, Mongoose and MondoDB as the database, hosted on Mlab.

pins in masonry style layout
The react-masonry package works to animate items efficiently into place in a way that is not possible with just css

The app replicates Pinterest’s masonry layout using react-masonry, allowing cards to create that cobblestone look, slotting into one another. Pins can be re-pined by new users, added to boards and have likes and comments posted.

The front-end was built using React-Router for pagination and to split the various aspects of functionality.

an individual pin in full-screen
An open pin with it’s source link credited and some comments underneath

The name was a joking placeholder on the observation that Pinterest’s once usable format was heavily modified and moved from it’s original format in the name of increased advertising and ‘engagement’. “Interest” meaning more click through’s instead of meaningful curation is replaced with “Apathy”.

user homepage
A user’s home page showing their boards and the three most recent pins

Rouge Like React

The final project on the old freeCodeCamp curriculum, a seemingly gargantuan task to push the React framework to create a procedural generated rouge-like dungeon crawler.

This was my second really big React project and also served as my first introduction to Redux.

The game is grid-based, displaying the user in roughly the middle of the map, rooms branding off in various directions. The player’s view is obstructed by default, showing only their immediate surroundings. The player can move around, pick up items and health packs and fight enemies by ramming into them, every time calculating damage for the enemy and player.

game screen showing disrupted view
The Main game screen with darkness enabled.
screenshot of a generated level

The game progresses through levels which are generated completely randomly each time with a set, or slightly varying number of health packs, enemies etc. The player can progress their weapon to deal more damage on each hit of the enemy and prepare them for the final boss (a really tough enemy!)

The game was balanced to make it actually challenging to play; there is a genuine trade off between getting more XP, health and risk factors going into the next level. The collectables and diminished view port incentivise exploration of the auto-generated labyrinths.

Games like this and the Game of Life are brilliant for learning fundamental data structures and data-visualisation methods in an intuitive way. Years later I found myself learning about such data structures, not knee-deep in C or Java, but by relating what I was reading back to this project.

The core data structure is a matrix; a two-dimensional array representing the rows and columns, each entry holding cell data. A single Board.js component is tasked with itirating over this array and painting a cell a particular colour, off-white if the cell is a floor, blue if it is a wall etc.

React then handles updates to this board by re-rendering after mutations apply to the array. This was a great project to learn Redux with, given that it is an ideal use case for Redux’s feed-forward immutable patterns.

Given that nothing on the board moves independently, there was no need for a sequencer or ‘game tick’, the only actions come from the player moving, this is one of the realisations that helps to break down the problem early one; you only have to listen to four key press’es effectively.

On player move, the action-creator looks to the cell the user wishes to move to and decides an action based on what is found there, it then calculates the next state of the board and updates it all at once.

The biggest challenge with this project was dynamically creating each level, every level, and every game had to be unique. A function was created that followed this basic process:

The centre of the board was found, a starting square of 9 x 9 was turned into ‘floor’ (the default cell being ‘wall’). This was designed to ensure there were no proximity collisions on level load, hoping that the user would not notice the centre of each map was the same.

A function for generating a single ‘room’ took in a direction as an argument and decided on a size (width * height) within constraints. Then, in the direction dictated, using the previous centre as a starting point, it would move it’s pointer a random number of cells across (within constraint). Then in the perpendicular direction it would also move a random direction but only to a maximum of two cells.

For instance if I was ‘moving’ right, the pointer would move to the right a random around between 2 and 7 and then move up or down between 0 and 2.

a diagram of the room generator function
This diagram shows how the room generator function to find the new centre would check valid cells and shift in the x and y directions to find the new centre.

This would then give us a new section of floor space which overlaps with the previous (thus extending the room) or, if operated by a wall, a door would be made at a random point to connect the two.

The last thing this function would do is make note if any value was outside of the board boundaries, this allowed the implementation of the last major function; the recursive path generator.

The recursive generator would, for each cardinal direction, keep generating rooms using the previous function, picking a new direction each time, allowing it to loop back on itself. It continues until the data being returned would signal that an outer edge had been hit, and the function would stop. This enabled truly unique levels every time.

All that remains is to randomly place a set number of entities per level; enemies, weapon upgrades, health packs and the exit point leading to the next level. A recursive function is used to do this, randomly picking a cell and calling itself again if that cell is occupied.

I also put some small quirks in the game to give it some personality such as the “you died” screen and the choice of weapons in the progression such as “Rock on a stick” and “The power of React itself”.

This being a project in summer 2017, it was built using an older version of both react and redux which is amusing in hind sight. How remembers React.createComponent({}) ? or lifecycle method based Redux store subscriptions?

a lifecycle method from the old redux

Looking back, this project was a key moment in my development career. Sure, the code is highly inefficient and messy by my current standards given that three years have passed as of time of writing. There were also some pretty bad mistakes and antipatterns which I learned from later on such as the un-pure functions called from within the reducer and functions which dispatch also in the reducer.

But the biggest effect this project had was that it allowed me to realise the size of a challenge I could overcome. Before starting, I had taken a break from code because I had not idea where to even begin with this challenge. I believed I would quickly burn out or waste weeks worth of time only to eventually fail, thus validating my imposter syndrome.

I can’t remember what exactly what let me get over this self-imposed block but I do remember the thought process, bringing together all of my experience and knowledge of algorithms to that point, and experiencing inspiration about the issue of procedural generation which lead me to start.

Direction Planner

The Directions App was an attempt to reconcile challenges around balancing complex responsibilities and needs in day-to-day life, inspired by a personal need.

A new take on the classic “work life balance” dichotomy, the Directions App presented all actions and responsibility as equal, and aimed to offer ‘mindless’ task direction without simply creating to-do lists.

This was a significant project to me, it was my first proper Redux application and allowed me to hone in React knowledge that I had previously only uses in fits and spurts.

Much later on, the notion of non-coercive task organisation and my findings from this project influenced the “traffic light system”, a component of my project Boots & the Future of Wellness.

View the live demo and repository with the links or read on to learn the backstory.

Backstory

Ok so we’ve all built To-Do lists, you learn the basics of a new MVC framework and can easily test your work. Then maybe, while continuing your learning, you make a version with data-persistence, probably with localstore or maybe something like Redis. Then if you really want to, you can set up a database and save the data permanently which is a great way to practice React-Express integration.

If you desire more in-depth productivity and planning tools there is no end or free software available to help you ‘do more, faster and smarter’ or ‘take control of your work’ or ‘plan every last detail’.

I found myself in a personal quandary. I, as ever, had a lot going on in my life. I was slowly coming to the realisation that just powering through; working endlessly, pushing myself beyond limits, just wasn’t working. Day’s blurred into one endless stream of “stuff”, with five things demanding my attention at once, an endless wish-list of things I’d like my life to have (or not have) “once this is all over”.

I would burn out, I was kept physically fit by running about all day but my health took a beating with regard to diet, sleep, and of course, mental state.

You are a machine

Everyone has at least some experience of living like this and maybe gave or received advice that sounds something like “You’re not a machine, you’re a person, you need time to breath and do other things”. I agree with this sentiment broadly but I think it misses something: a ‘machine’ of most types does not run endlessly, without “rest”, or without “breathing”.

The definition of machine is broad but lets generalise of a second, imagine an abstract “Machine”. A machine can run for a duration of time then needs repair. A machine can run at sustainable levels for long periods. A machine can be pushed beyond it’s quoted physical limits but with the cost of quicker time-to-fail or malfunction.

I don’t know about you but that sounds like a pretty good description of a person to me.

A person is a biological system that has needs and conditions with affect their behaviour and abilities in their ‘functioning’, living day to day life. To take some easy exemplars, if you don’t sleep enough, you can put in all the extra hours that you like but won’t necessary be as productive. If you write something while caffeinated it may have a different read to something written in a more subdued state. If you do work while a little tipsy you may find you peak in creativity and inspiration but make sloppy mistakes and quickly drop bellow productive levels.

Here’s one I still struggle with, if you are stuck on a problem, taking time away, rather than trying to power through, can let your subconscious process it in ways you are unable to, leading to faster solutions. As intuitive as it may be to step away while the clock is ticking.

“This is not a task-list”

So how exactly do you balance things when you have five ticking clocks and now, the pressure to do this “self care” thing you just learned about?

You could try a simple planner and a to-do list but these both have the issue of making everything a “task” to finish. Is walking in a park a task? Reading a few pages before bed? What about things that repeat, like if you are job searching and need to do a little every day? Do you continue on weekends? How much do you do a day? How do you measure it?

The Directions App was an attempt at doing just this, it ordered all items with equal weight, utilising a tag system to differentiate them.

  • Priority: this denotes actual task items, things which need to be completed
  • Ongoing: this is used for tasks which repeat for a duration of time
  • Fun: used for recreational activities
  • Health: used for activities which benefit health and well being
  • Social: used to section social activities

In this way you could combine which would show items denoted by which filters were active. Looking to do something recreational but also healthy? Turn on ‘Fun’ and ‘Health’. Set some time aside for task work? Use ‘Priority’, and so on.

Looking back

I still have a fondness for this app even if I didn’t actually end up using it all that much after a while. It was a useful way to explore things which exist between boundaries such as something which is fun and enjoyable to do but is still ‘work’. I realised that by assigning everything a tag and grouping them together, I could see the interlink between various tasks but it didn’t work so well to actually designate time, to come up with that magic formula where everything is balanced and just falls into place.

On a stylistic note, it was useful for me to develop emerging styles and layout patterns which would be refined further later on, even if it looks somewhat garish now.