How I saved one month of work to an entire squad


Introduction to the feature

In every company, finance teams define budgets once a year, following a budgetary exercise.

The goal was to model the budgetary exercise in our software, so the companies using our software could

  • link their spendings (that we collected on our platform) to budgets
  • see the budgets consumptions in real time, and see if they spent as planned (or not)

Here is how a simple annual budget could look like:

Relation

In this example, the budgets correspond to different teams in the company, and each budget amount is splitted into expense categories.

Here are the simplified entities for this feature.

type BudgetaryExercise = {
  id: string;
  startDate: string;
  endDate: string;
  budgets: Budget[];
};

type Budget = {
  id: string;
  budgetaryExerciseId: string;
  amountByExpenseCategory: Map<ExpenseCategoryId, MonetaryValue>;
};

Development stages

We started by implementing a PoC, with the entities describe above (actually it was a bit more complex, but the core idea is there).

The PoC went well, and we wanted to work on a first version of this feature for our users.

The idea was to start with a simplified version of this feature, with no split by expense categories, and add this split by expense categories later.

When ? maybe in one month, three month, one year, … who knows.

The problem

The feature was a bit more complex than explained.

One important missing concept here is budget breakdown.

A breakdown is the result of a computation made on a budget of the following amounts:

  • already used (payments)
  • committed (amount “locked” for planned payments)
  • available
  • used exceeded
  • committed exceeded

And a breakdown can be by expense category.

In the PoC, we’ve already built a big part of the logic of the feature, with budget splited by expense categories.

What to do ?

From this moment, the idea was to rewrite everything without the concept of “split by expense account”.

But it felt wrong to me. Why would we have to simplify everything, to add the split by expense account later ?

  • It seemed that there would be a similar amount of work to finish the feature with the simplified version (rewrite current logic + write the remaining part of the feature) and to finish the feature with the split by expense category.
  • But in few months, it would complicated to add the split by expense category
    • It would introduce breaking changes.
    • The feature might evolve since there, so we would not be able to copy/paste the logic from the PoC.
    • There would be data in production to migrate to the new model, and here we are talking about money and analytics that drives the companies It’s very critical.
    • The unit tests would have to be rewritten: we might break something without knowing it.

My idea was to keep the “complex logic”, with the split by expense category. But expose a simplified version from the API, so that

  • The frontend developpers could build the simplified feature (without the split)
  • But we keep the split in the logic and data model (in the backend)

It’s easy to fake the “no split” using the “split” logic and model.

Instead of:

const split = new Map([
  ["Freelance and experts", 200],
  ["Tools and subscriptions", 800],
  ["Server and COGS tools", 0],
]);

We can have:

const split = new Map([["default", 1000]]);

Which means there is still a split, but only in a “fake” expense category called "default".

Simplified budget

type SimplifiedBudget = {
  id: string;
  budgetaryExerciseId: string;
  amount: MonetaryValue;
};

Instead of a Map to store the amount for each expense category, only one amount is necessary, because there is not split in this simplified version.

From SimplifiedBudget to Budget

amountByExpenseCategory = new Map([["default", simplifiedBudget.amount]]);

The Budget logic, with the split, can be used with this "default" expense category.

From Budget to SimplifiedBudget

amount = amountByExpenseCategory.get("default");

The SimplifiedBudget can be sent to the frontend.

The adapter pattern to the rescue

Instead of having the BudgetController calling directly the BudgetService (that contains all the logic with split by expense category), which would mean the API respond with a Budget, like this:

Relation

There would be an adapter in between that would do the conversion from a SimplifiedBudget to a Budget, and from a Budget to a SimplifiedBudget.

Relation

The BudgetController calls the BudgetService.

But the SimplifiedBudgetController calls the adapter (SimplifiedBudgetService) that contains the logic to convert a SimplifiedBudget to a Budget, and the adapter then call the BudgetService.

  • SimplifiedBudgetController doesn’t know what is a Budget or BudgetService.
  • BudgetService doesn’t know about SimplifiedBudget
  • BudgetController is usable in parallel, to use Budget with the split by expense category (already ready for the future)

Relation

In the code, SimplifedBudgetService has a similar interface to BudgetService, but using SimplifiedBudget instead of Budget.

export interface BudgetService {
  findById: (id: string) => Promise<Budget | undefined>;
  deleteById: (id: string) => Promise<DeleteBudgetByIdResult>;
  create: (dto: BudgetDto) => Promise<CreateBudgetResult>;
}

export interface SimplifiedBudgetService {
  findById: (id: string) => Promise<SimplifiedBudget | undefined>;
  create: (dto: SimplifiedBudgetDto) => Promise<CreateSimplifiedBudgetResult>;
}

There is no deleteById because in the PoC we decided that budget were not deletable.

Benefits

  • It keeps the Budget entity and logic, but provides a SimplifiedBudget for a simplified version of the feature.
  • It’s super light and quick to implement, the adapter code is very simple.
  • A data migration is always complicated and stressful, whereas the adapter here is easily testable.

Was it worth it ?

I switched to another team after the first version and the adapter. The next year I heard from the team that they released the split by expense categories, and that this adapter save them a lot of time.

So yes, it was worth it 🚀