25 May 2026·12 min read·Drafted with Claude · human-edited

One Business Central payroll engine, five countries: why we made country packs data, not code

How we built one Business Central payroll engine for India, UAE, KSA, Netherlands and USA — without forking the codebase per country. The architecture trade-offs of treating countries as configuration data rather than code paths.

SourceForge BC Practice
Business Central engineering

Most payroll add-ons for Business Central are one country with extras bolted on. The Indian one knows about EPF and ESI. The UAE one knows about WPS and EOSB. The US one knows about FICA and Medicare. They share a name, maybe a vendor, and not much else. When a business runs payroll in three or five countries, they end up with three or five separate systems — each with its own permission model, its own consolidation pain, its own upgrade schedule, its own bugs.

We did not want to build that. We built one engine and made countries pure configuration data. The result ships today as SourceForge Global Payroll & HR for Business Central with India, UAE, Saudi Arabia, Netherlands and USA packs at launch. This post is about the architecture decision behind it — what it cost us and why we think it pays back the moment a customer reaches their second country.

Why one-country-per-product became the default

Payroll add-ons calcify around the country they were first written for. A consultant starts with India because that is where their first customer is. The earliest pay elements get hard-coded — BASIC, HRA, EPF, ESI — because at the start it feels faster to write ESIAmount := BasicSalary * 0.0075 in AL than to design a configuration table for it. A year later, when the second customer is in the UAE, the team needs gratuity and GPSSA. Now there is a choice: refactor the original code into something configurable, or shim UAE features alongside. The shim wins every time, because it ships faster.

A few iterations of this and the codebase is a mess of if Country = 'IN' then ... else if Country = 'AE' then .... Adding the third country means understanding the first two. Adding the fourth means understanding the first three. The product becomes unmaintainable, and the team's response is usually to fork it — separate India edition, separate UAE edition. Same name, same vendor, separate codebases. The customer pays for the fork in licence and pays again in integration.

We did not want to be that vendor. So we started from a different place.

The architectural decision

The engine knows nothing about countries. It knows about Pay Elements, Wage Bases, Statutory Contributions, Tax Models, Brackets, Overtime Rules, Leave Types, Holiday Calendars and Payment File Definitions — none of which mention any specific country. A country pack is a set of rows in those tables, plus a flag saying which jurisdiction the rows are for. Activating a country pack inserts rows. It does not run any country-specific code, because there is no country-specific code.

The discipline is this: anywhere the codebase is tempted to write if Country = 'IN' then, we instead define a configuration mechanism that lets a row in a table express the same thing. Sometimes that means a new flag on an existing table. Sometimes it means a small new table. The principle is: if a feature is country-specific, the code should not say so — the data should.

What a country pack actually is

The India pack (IN-PACK) is, concretely, the following row inserts at install time:

  • One row in SGP Country (IN, India, INR, en-IN).
  • One row in SGP Country Pack (IN-PACK, IN, 1.0).
  • Roughly sixty rows in SGP Pay Element covering Basic, HRA, Conveyance, Special Allowance, employee/employer PF, ESI, Professional Tax by state, Income Tax (under both regimes), Gratuity accrual, Bonus, LOP, etc. Each row has a calculation method (Fixed / Percentage / Formula / Slab / Attendance Proportionate / Piece Rate / Balance Remainder / Lookup) and a formula expression where applicable. Nothing is hard-coded.
  • Roughly twelve rows in SGP Wage Base Definition covering BASIC, GROSS, PF_BASE, GRATUITY_BASE, TAX_BASE, OVERTIME_BASE and so on.
  • Six rows in SGP Statutory Contribution for PF, ESI, PT, Gratuity, Bonus and Labour Welfare Fund. Each carries the contribution type, the wage base it sits on, the employer and employee percentages, the ceiling, and a link to the pay element where the result lands.
  • Two rows in SGP Tax Model for the Old Regime and New Regime, each with their respective bracket rows.
  • One row in SGP Holiday Calendar for India with the gazetted holidays.
  • Two rows in SGP Payment File Definition for NEFT and RTGS, with the right column layouts for Indian bank uploads.

That's the entire India pack. No AL code runs that is specific to India. The same applies to the UAE, KSA, Netherlands and USA packs — each is a few hundred rows across the same set of tables, with the country code stamped on every row.

Why this matters: a worked example

Consider how gratuity is calculated. In India, the formula is roughly Last Basic × 15 / 26 × Years of Service. In the UAE, it is more complicated — Last Basic × 21/30 × Years of Service for the first five years, then 30/30 for years beyond five, with caps and reductions for resignation versus termination. In the codebase that hard-coded India first, this becomes a fork point. The UAE consultant cannot reuse the India gratuity codeunit; they write a new one, with its own subscribers and its own tests.

In our engine, both formulas are rows in SGP Pay Element with Calc Method = Formula. The India formula reads BASIC * 15 / 26 / 12 as a monthly accrual. The UAE formula reads IF(YEARS_OF_SERVICE > 5, BASIC * 30/30 / 12, BASIC * 21/30 / 12). The Formula Evaluator parses both. The Statutory Engine fires both. Adding a third country's gratuity rule is one more row.

The same applies to overtime. India's Factories Act says weekly hours over 48 trigger overtime at twice the ordinary rate. UAE's Labour Law says daily hours over 8 trigger overtime at 1.25× on weekdays, 1.5× on weekly-off, and 2× on public holidays. The US has the FLSA test of 40 hours/week at 1.5×. Three different rules, three different rate structures. In our engine, all three are rows in SGP Overtime Rule with rate bands attached. The Overtime Engine reads the rule effective for the employee's establishment and applies it. No country-specific code path.

The trick: effective-dated everything

Statutory rates change. India's PF ceiling was ₹6,500, then ₹15,000, with the proposal to revise to ₹21,000 active in policy discussions for years. UAE's GPSSA percentages adjust periodically. US Medicare added the 0.9% surtax above $200k in 2013. A payroll engine that overwrites the old rate with the new one loses the ability to recompute history accurately — and tax authorities sometimes audit two years back.

So every rate-bearing row in our schema is effective-dated. The SGP Statutory Rate table is one row per rate change with Effective From and the new percentages. The Statutory Engine reads the rate effective for the period being processed. Adding the latest rate change is one new row. The old row stays. A recomputation of a six-month-old period uses the rate that was in force then.

This sounds obvious, and it is — but it requires discipline at the schema level from day one. Retrofitting effective dating into a payroll engine after the fact is hard, and we have seen vendors fail to do it.

Multi-regime tax: how one country becomes two

India has two simultaneous income-tax regimes — the Old Regime with traditional deductions (80C, 80D, HRA, etc.) and the New Regime with lower rates but minimal deductions. Each employee chooses one per fiscal year. A naive payroll engine handles this with hard-coded branches: if Regime = 'OLD' then ApplyOldRegimeCalc else ApplyNewRegimeCalc.

In our engine, both regimes are rows in SGP Tax Model with Model Type = Progressive Brackets. Each has its own brackets in SGP Tax Brackets. A SGP Tax Regime row points at each, with one marked as the default. The employee card has a Tax Regime field; if blank, the default applies. The Tax Engine reads the model the employee's regime points to and applies its brackets. Switching regimes is a field change, not a code path.

The same pattern works for any jurisdiction where employees can choose between tax structures. We do not have a hard-coded notion of "India has two regimes". The schema just supports "a country can have multiple tax models with regime selection".

Layered taxes: the US case

The US is harder. Income tax is layered: Federal + State + Local (sometimes city, sometimes county). Each layer has its own brackets, its own annualisation, its own standard deduction. An employee in Chicago pays Federal income tax, Illinois state income tax and Chicago city income tax simultaneously, on the same wages. The naive approach is to write a US-specific calculation codeunit that hard-codes the layering.

We modelled it as a Layer field on SGP Tax Model with values Federal / State / Local / National / Combined. The Tax Engine evaluates every active tax model whose layer is in scope and sums the results. The US Federal model is one row. Each state has its own model. Local jurisdictions that levy income tax have their own. Adding a new state is a model row plus its bracket rows. Adding a new municipality is the same.

This generalises beyond the US. The UK has PAYE + NI. India has TDS at the Federal layer and Professional Tax at the state layer. The same Layer field handles all of them. We did not write US-specific code; we identified a pattern (multi-layer income tax) that several countries exhibit and supported it once.

What it costs

The honest trade-off: this architecture is slower to build than the hard-coded alternative for the first country. The Formula Evaluator alone took us about three weeks. The effective-dating discipline added complexity to every table. The Pay Element validator that detects circular references is a real piece of engineering. If we had hard-coded the India calculations, we could have shipped India a month sooner.

The pay-back starts at country two. The UAE pack took us about eight working days to build, end to end — and most of that was learning UAE labour law, not writing code. KSA was four days, because most of the patterns were shared with UAE. Netherlands was six days. The US was twelve days because of the layered tax complexity, but again — almost all of it was research, not engineering.

Crucially, the team that built the UAE pack did not need to understand or modify any of the codeunits the India pack relies on. They worked exclusively in configuration tables. The blast radius of a country pack change is one country. We have never had a UAE configuration change break India.

What this means if you are evaluating us

If you run payroll in one country and have no near-term plan to add a second, our architecture is over-engineered for your needs. A pure-India payroll add-on with hard-coded EPF and TDS would meet your requirements with less to learn. We are honest about this on every discovery call — and several times have recommended a competitor for a single-country deployment.

If you run payroll in two or more countries — or expect to within twelve months — the configurability becomes the point. Adding the next country becomes a configuration project, not a software project. Statutory rate changes become a row edit, not a deploy. Auditors get effective-dated history without anyone having to back-port a fix. Your finance team uses one system, one permission model, one consolidation.

The honest summary: we built the engine we wanted to maintain for the next decade, knowing the first customer's go-live would be slightly slower because of it. We think that is the right trade-off when the product needs to grow into a country count we cannot predict at design time. And we think the next decade of Business Central payroll add-ons will increasingly look like this — not because the architecture is novel, but because the pain of the alternative compounds with every country added.

Written by
SourceForge BC PracticeBusiness Central engineering

Published 25 May 2026 by SourceForge Software Services Pvt Ltd. Replies, corrections and follow-up questions: info@sourceforge.in.

Have a project that touches what you just read?

The blog exists because we'd rather show our thinking than pitch it. If something here resonated, let's talk about how it applies to your situation.

WhatsAppCall us