AIB Digital — Access · Integrate · Bloom
Senior .NET Engineer · Islamabad

Software that cannot afford to break.

Land-records digitization for a provincial authority. An earned-wage-access platform on the i2c gateway. An artwork and exhibitions system for a culture ministry. Payroll in daily use. A retail point of sale you can try and buy today. An overseas recruitment platform you can open right now at kafeelmgt.com. Four years of systems where a failure is somebody's wages, somebody's land title, somebody's audit, or somebody's job in another country.

claim system problem pattern code

Hover a ring. Everything on this page opens.

01

Where the work happened

Four roles since September 2022 — but the roles are the least interesting row. Underneath them is what was actually in my hands each month. Read down a single column and you see the stack as it stood on that date; read across a lane and you see how long something was genuinely used rather than how recently it was learned.

2022 2023 2024 2025 2026 2027 ROLE AMAN — Full Stack Engineer. Sep 2022 – Jan 2024 (1 year 4 months) AMAN Full Stack Engineer PayActiv — Software Engineer. Feb 2024 – Sep 2025 (1 year 7 months) PayActiv Software Engineer MPN — Senior Dotnet Developer. Oct 2025 – May 2026 (7 months) MPN Senior Dotnet Developer Solochoicez — Full-Stack .NET Developer. Jun 2026 – now (5 months) Solochoicez Full-Stack .NET Developer .NET platform Front end Data Payments & distribution Cloud & reporting Beyond .NET C# C# — Sep 2022 – now (4 years 2 months) ASP.NET ASP.NET — Sep 2022 – now (4 years 2 months) .NET Core .NET Core — Feb 2024 – now (2 years 9 months) Entity Framework Entity Framework — Sep 2022 – Jan 2024 (1 year 4 months) Entity Framework — Oct 2025 – May 2026 (7 months) Angular Angular — Sep 2022 – Sep 2025 (3 years) three years, across two employers jQuery jQuery — Sep 2022 – Jan 2024 (1 year 4 months) over the ASP.NET pages that already existed JavaScript JavaScript — Sep 2022 – now (4 years 2 months) SQL Server SQL Server — Sep 2022 – now (4 years 2 months) T-SQL · indexing T-SQL · indexing — Sep 2022 – now (4 years 2 months) Microservices Microservices — Sep 2022 – Jan 2024 (1 year 4 months) Microservices — Feb 2024 – Sep 2025 (1 year 7 months) Microservices — Jun 2026 – now (5 months) three employers i2c payment gateway i2c payment gateway — Feb 2024 – Sep 2025 (1 year 7 months) money movement, so retries had to be safe Azure · DevOps Azure · DevOps — Sep 2022 – Jan 2024 (1 year 4 months) Azure · DevOps — Feb 2024 – Sep 2025 (1 year 7 months) Azure · DevOps — Oct 2025 – May 2026 (7 months) Azure · DevOps — Jun 2026 – now (5 months) every role RDLC reporting RDLC reporting — Sep 2022 – Jan 2024 (1 year 4 months) RDLC reporting — Jun 2026 – now (5 months) SSRS SSRS — Oct 2025 – May 2026 (7 months) PHP · Laravel PHP · Laravel — Sep 2026 – now (2 months) the AIB Digital work, and the live system below today
.NET platform Front end Data Payments & distribution Cloud & reporting

How to read the gaps, and the repeats. jQuery stops at the start of 2024 because that is when the ASP.NET estate it was layered over stopped being my daily work — not because it was forgotten. Angular runs straight through a change of employer, which is the only reason three years of it is one bar instead of two. The lanes worth looking at twice are the broken ones that keep coming back: microservices at three employers, Azure and DevOps at all four, RDLC reporting at the first and again at the current one. A technology you are brought in to use a second time, years later and somewhere else, is a different claim from one you touched once. Nothing is drawn on this chart that I cannot point at a project for.

Current role, .NET across the stack. This is also the period the AIB Digital work runs alongside, which is why the live system further down this page is Laravel rather than .NET — a second stack kept deliberately, so that a small project is not carrying an enterprise framework it does not need.

What four years of this actually teaches. Not the syntax. It teaches that a query which is fine at fifty thousand rows is an incident at five million; that an API failing predictably is worth more than one that rarely fails; and that the deployment is part of the design, because on a system people use daily, downtime is a design flaw rather than an inconvenience.

C#.NET CoreASP.NET CoreSQL ServerREST APIsClean Architecture

Senior .NET: ASP.NET MVC and .NET Core services over SQL Server, with enterprise reporting and third-party integration.

Reporting is the part nobody writes about. SSRS and RDLC are unfashionable and completely non-negotiable, because an invoice, a payslip or a government return has to come out of the system in precisely the layout the organisation expects — not approximately, and not in a format somebody then rekeys. A reporting query also has to be prevented from taking the transactional system down with it, which is where indexing stops being theory.

The integration half carries the lesson that someone else owns the other side: timeouts you do not control, schema changes you learn about from a failure, and sandbox behaviour that differs from production in exactly the way that matters.

.NET CoreASP.NET MVCEntity FrameworkAzure DevOpsSSRSRDLCSQL indexing

Earned-wage access: an employee draws pay they have already earned, before payday. The platform sits between an employer's payroll and a payment gateway, which means it moves real money on behalf of people who are usually drawing early because they need it that week.

Why that changes the engineering. The happy path is trivial. Everything interesting is in the failure modes, and they are the expensive kind: the gateway may accept a payment and fail to tell you, a connection may drop after the money has already moved, and a retry that looks harmless can pay somebody twice. So the work is not preventing failure — it is deciding what is true when two systems disagree, recording each attempt before it is made, reconciling after, and making a repeat of the same request provably the same request rather than a second one.

Angular, the first of two. The client side here was Angular against .NET Core APIs: typed service layers over REST, reactive forms for data entry where a wrong value moves money, and route guards tied to the same authorisation the API enforced. The rule that stuck is that the front end must never be the only thing stopping an invalid action — it is the place you make the valid action obvious, while the server stays the thing that actually refuses.

Angular.NET CoreMicroservicesi2c gatewayIdempotencyReconciliationREST APIsSQL Server

Full stack across government and enterprise systems, including the artwork and exhibitions platform for the UAE Ministry of Culture & Art.

Government work sets a different bar. Not a higher standard of cleverness — a higher standard of being able to explain yourself. A record has to survive review, which means who changed what, when, and under whose authority is part of the data model rather than a log file somebody might have switched on. That is where the habit of treating writes as events instead of updates came from, and it is the same pattern running underneath the live system at the bottom of this page.

Angular and jQuery in the same building. New work took an Angular front end; the existing ASP.NET estate took jQuery, because those applications were running, earning, and not going to be rewritten to satisfy anybody's preference. Knowing when not to replace something is a large part of this job, and inheriting a working system teaches it faster than starting one.

Bilingual was a data-model problem rather than a labels problem — which is the kind of requirement that looks like a translation task on the estimate and is not.

AngularASP.NETjQueryJavaScriptEntity FrameworkSQL ServerAudit trail designBilingual data model
02

Systems delivered

Open one to reach the engineering problem underneath it, then the pattern that solved it. Three levels down there is a diagram; four levels down there is code. The first two are running right now — one is a product with a demo you can sell from, the other is live on its own domain. Open them and check.

A point-of-sale and back-office system for shops with one till or several branches: checkout, cash drawer and shifts, returns, stock per branch, purchasing, expenses and reports, for as many businesses as sign up, each walled off from the others. Designed and built end to end by me and sold as a product — prices, screens and the live demo are here.

A sale looks like one row. It is four changes that have to agree: the money in the drawer, the stock on the shelf, the receipt-number sequence, and the audit trail. A sale that charged the customer but did not take the stock, or burned a receipt number and then failed, is worse than a sale that never happened — because now the books are wrong and nothing says so.

And the counter is the worst network in the building. The cashier presses Charge, the connection drops before the answer comes back, and the customer is standing there. The cashier will press it again. The only acceptable outcome is one sale.

Every checkout carries an idempotency key. A key already seen returns the original sale; the same key with a different cart is refused. Only a new key opens a transaction, and everything the sale touches is written inside it.

// Modules/Sales/SaleService.cs — the real checkout
// A retry of a sale that already went through returns the original sale.
if (await LoadExistingAsync(idempotencyKey, ct) is { } existing)
    return existing.RequestHash == hash
        ? new CheckoutResult.Completed(existing, Replayed: true)
        : new CheckoutResult.Problem(409, "Conflict",
            "This checkout key was already used for a different sale.");

// ---- Save: number, stock and sale commit together or not at all ----
await using var tx = await db.Database.BeginTransactionAsync(ct);
var number = $"S-{await sequences.ReserveAsync(tenantId, SequenceNames.Sale, 1, ct) + 10000}";
db.Sales.Add(sale);
await stock.PostAsync(tenantId, request.BranchId, StockMovementType.Sale, stockChanges, ...);
db.AddAuditEvent(AuditActions.SaleCompleted, ...);
await db.SaveChangesAsync(ct);
await tx.CommitAsync(ct);
Charge+ Idempotency-Key keyseen? return the original sale — no new charge one transaction — all of it, or none sale · lines · payments · receipt number stock movements (append only) audit entry (cannot be edited)
Idempotent checkoutOne transactionGap-free receipt numbersdecimal(19,4) money

Two cashiers scan the last bag of rice at the same second. The obvious code reads one in stock on both tills, both check that one is enough, and both sell it. Stock goes to minus one and the shelf is empty for the next customer whose bill says otherwise.

Checking before writing cannot fix this, because the check and the write are two moments and the other till lives between them. The check has to be the write. And because stock is a ledger rather than a number, every change also records what was left afterwards — so when the shelf and the screen disagree there is a trail to walk, not a figure to argue with.

One statement takes the row lock, refuses if the result would go below zero, moves the weighted-average cost in the same breath, and hands back what it left. No row means it was refused; the caller turns that into “only N left” and nothing is charged. Rows are locked in variant order, so two multi-item sales cannot deadlock each other.

-- Modules/Inventory/StockService.cs — the only way stock changes
UPDATE [inventory].[StockBalances] WITH (UPDLOCK, ROWLOCK)
SET [OnHand] = [OnHand] + {delta},
    [AverageCost] = CASE
        WHEN {cost} IS NULL THEN [AverageCost]
        WHEN [OnHand] <= 0 OR [AverageCost] IS NULL THEN {cost}
        ELSE ROUND(([OnHand] * [AverageCost] + {delta} * {cost}) / ([OnHand] + {delta}), 4)
    END
OUTPUT inserted.[OnHand], inserted.[AverageCost]
WHERE [BranchId] = {branchId} AND [VariantId] = {variantId}
  AND ({allowNegative} = 1 OR {delta} >= 0 OR [OnHand] + {delta} >= 0)

Each call also appends a StockMovement carrying BalanceAfter. A stock count uses those movements to work out what was on hand at the moment each item was counted, so sales made while the count is running do not show up as shortages.

Row-level lockingStock ledgerWeighted-average costDeadlock-safe lock order

Every shop counts the drawer at night, and almost every shop is a little out. The question that matters is not how much but why, and by morning nobody remembers. Cash came in from sales, went out for refunds, a paid-out for the courier, a drop to the safe at lunch — and none of it was written down where the count could see it.

So the drawer is a shift with its own record. The opening float, every cash sale and cash refund, paid-ins, paid-outs and safe drops are entries against it; expected cash is derived from them, never typed. The cashier counts note by note, the difference is stored beside the expectation rather than replacing it, a shift that does not balance will not close without a reason, and a manager reviews it afterwards.

Derived expected cashDenomination countReason requiredManager review

The hosted plans put many businesses on one database. One forgotten WHERE clause and a shop sees its competitor’s sales. So isolation is not left to each query: every tenant-owned table carries a global filter, and a request with no signed-in business reads nothing at all.

Inside a business the risk is quieter. A manager who may add staff could create a role with powers they do not have, put a new account on it, and sign in as that account. So a role can only be given permissions its editor already holds; every permission is its own server-side policy, because hiding a button is not security; and branch access is checked on the server for every branch-scoped request.

Multi-tenant query filtersNo privilege escalationPer-permission policiesBranch scoping
.NET 10ASP.NET CoreEntity Framework CoreSQL ServerAngular 22TailwindModular monolithIntegration tests on real SQL ServerGitHub ActionsDocker

The public platform for an overseas manpower recruitment firm working since 2013 — placing workers from Pakistan, India, Nepal and Bangladesh into ten destination countries, across five skill bands from daily-wage labour up to site management. Designed, built and deployed by me, and running in production now at kafeelmgt.com.

A recruitment firm has two kinds of visitor and they are not looking for the same thing. An employer wants evidence of screening and a deployment time they can plan around. A worker wants evidence that the contract is real, that it will be shown before they get on the plane, and that somebody picks up the phone after they land.

Write the site for the employer and it reads as a labour brokerage, which costs you the candidates the business runs on. Write it for the worker and the employer cannot find the commercial answer they came for. So the structure itself has to carry both — one platform, two declared paths — rather than one voice trying to cover the gap.

Underneath that sits the harder thing: in this sector trust is not assumed. Somebody is being asked to leave their country on the strength of what a page says. Persuasion does not fix that; showing the process does. So the full eleven-step recruitment sequence is published rather than summarised, the trade tests and medicals are named, and the post-deployment worker assessment is stated as a step — because the most reassuring thing an agency can put on a page is what it does after it has been paid.

Two-audience IA11-step process, published5 skill bands10 corridors

Both paths end at one form, and that is where a recruitment site usually leaks. An enquiry that arrives as a name and a paragraph of text tells you nothing actionable, so somebody replies asking which side they are on, which country, which trade — and across a time-zone gap each of those round trips costs a day. On a lead that was already only half-decided, days are what kills it.

So the form resolves those questions before it is sent: which path, which destination corridor, which skill category, and — carried invisibly — where the lead came from and which page it started on. One submission, and the enquiry is already routable. The bot trap is a honeypot rather than a puzzle, because a candidate on a cheap phone on a weak connection should never be asked to prove they are human.

Path + corridor + trade capturedLead-source attributionHoneypot, no CAPTCHA

Hand-written HTML, one stylesheet and plain JavaScript — no React, no Vue, no build step shipped to the browser. Seven routes, served as static files from Cloudflare's edge, with the stylesheet cache-busted by content hash so a deploy is instant and a returning visitor is never served a stale layout.

That was chosen, not settled for. The audience is largely on mid-range Android phones on mobile data in Pakistan and the Gulf. A framework bundle buys the developer convenience and charges the visitor for it, and here the visitor is the one person the site cannot afford to lose. The interactive parts — the skill drawers, the corridor map, the counters — are small amounts of vanilla JS, which is why the page is readable before any of it has run.

HTMLCSSJavaScriptCloudflare PagesStatic, edge-servedGA4Hashed assets
JavaScriptCSSInformation architectureSEODomain & DNS go-liveCloudflare

Moving land ownership records from paper into a system a provincial authority and its citizens both depend on.

A land record is a legal claim. The question is never only what does it say now — it is who changed it, when, on whose authority, and what did it say before. A system that updates a row destroys the only thing that makes it trustworthy.

And the consequences are not administrative. A land record decides whether a family can sell, inherit, mortgage or build, and in a dispute the record is the evidence. Two people will appear holding claims to the same land, and the system's job is not to decide between them — it is to be able to show, without argument, exactly what was recorded, when, and by whom. That is a different engineering goal from storing the right answer.

So the record cannot be overwritten. Every change is an entry, and current state is derived rather than stored and hoped for.

Writes become events, not updates. State is read from the sequence, and audit trail plus access control stop being features bolted on at the end — they become the shape of the data.

event 1transfer in event 2correction event 3mutation derive()no stored total current owner+ full history intact APPEND ONLY — NOTHING OVERWRITTEN

The live demo at the bottom of this page runs on the same pattern, which is why you can click through it and watch the history build.

Digitization sounds like typing. It is not. The source is decades of handwritten registers, and paper of that age does not agree with itself. The same person is spelled three ways across three books. The same piece of land is described by one boundary in one register and a different one in another. Inherited shares arrive as fractions that have been subdivided across generations and do not always add to a whole.

The tempting thing is to clean it — pick the likeliest spelling, reconcile the boundary, round the share. That is the one thing the system must not do, because a tidy record that quietly chose a winner is worse than a messy one: it looks authoritative while having silently taken a side in somebody's future dispute.

So ambiguity has to be representable. The model needs somewhere to say the paper says two things here and carry that forward, rather than forcing a confident answer at import time and losing the fact that a question ever existed.

Ambiguity as dataSource fidelityName variantsFractional shares

A citizen at a counter does not arrive with a record ID. They arrive with a name, a father's name and a village, and at least one of those will not match what is stored — because the register spelled it differently, or because it was transliterated differently.

That makes search the actual product. An exact-match query is useless here, and a looser one over a table of that size is the kind of query that is fine in testing and becomes an incident in production. This is where indexing stops being a tuning exercise and becomes design: what you index, what shape you store names in so they can be matched at all, and which queries you are prepared to refuse rather than let run.

SQL ServerT-SQLIndex designQuery plansSearch over millions of rows
.NETSQL ServerAudit trailAccess controlIndexing

An employee draws wages already earned, ahead of payday. The platform sits between an employer's payroll and a payment gateway.

Every request crosses a boundary you do not control. The gateway may accept a payment and fail to tell you. The connection may drop after the money has moved. A retry that looks harmless can pay somebody twice.

And the person on the other end is drawing their own wages early, usually because something has already gone wrong that week. A failed transfer is not a support ticket to them — it is rent, or a prescription, on the day it was needed. That changes what "handled the error" has to mean: telling somebody the payment failed is only acceptable if it is true, and the one thing you cannot do is say "failed" about money that actually left.

The hard part is not the happy path. It is deciding what is true when two systems disagree, and making a repeat of the same request provably safe.

An API that fails predictably is worth more than one that rarely fails. Each attempt is recorded before it is made and reconciled after, a repeat is recognised as the same attempt rather than a new one, and the ledger is the authority — not the gateway's response.

Angular.NET CoreMicroservicesi2c gatewayIdempotencyReconciliation

Before any money moves there is a number, and that number is the whole product: how much of this month's wages has this person already earned but not yet been paid. It is derived — hours worked so far, minus what has already been drawn, minus fees, against a policy the employer sets and can change.

The trouble is that the inputs arrive from someone else's payroll, in their format, on their schedule, and they get restated. Hours are corrected. A shift is reclassified. Somebody's leave is entered three days late. Which means yesterday's figure can turn out to have been too high — and unlike a reporting error, this one has already been spent.

So the entitlement cannot be a cached number that somebody writes and trusts. It has to be re-derivable from its inputs at any later date, and the system has to be able to show how it reached the figure it offered at the moment it offered it. Otherwise the first time an employer disputes a balance, there is no answer — only a number.

Derived entitlementRestated inputsEmployer policyRe-derivable, not cached

Artwork, artists and exhibitions held as a government record — catalogued, moved between venues, lent out, and reported on.

A catalogue answers what do we have. That is the easy half, and it is the half most systems stop at. The real question a culture ministry has to answer is where is it, who has it, and what condition was it in when they took it — because the objects physically move, and some of them do not belong to the ministry at all.

A piece goes out to an exhibition, then to a second venue, then to conservation, then into storage. At each handover somebody accepts responsibility for it and somebody records its condition. Months later a scuff is noticed. The only question that matters then is which leg of that journey it happened on — and that is answerable only if every movement and every condition note was recorded as its own entry at the time, by whoever was standing in front of the object.

Which is why the location cannot be a field on the artwork. The moment you store current venue as a column and update it, you have an object that is somewhere, with no account of how it got there — and a loan agreement you cannot evidence.

Movements are events. Location is read from the most recent one rather than stored, condition reports and custody attach to the movement that caused them, and the history is the sequence itself. It is the same append-only shape as the land records, reached independently and for the same reason: both are records whose value is that they can be proved, not merely read.

EVERY HANDOVER IS AN ENTRY — LOCATION IS DERIVED acquired condition A lent · venue 1 condition A returned condition B — scuff storage condition B current location = last event damage is attributable

Teams usually treat bilingual as an interface job: extract the strings, hand them to a translator, flip the layout for right-to-left. That handles the buttons. It does not touch the actual difficulty, which is in the content.

An artwork’s Arabic title and its English title are often not translations of each other — they are two names the work genuinely has, and the artist or the curator chose both. An artist’s name has an established Arabic form and an established Latin transliteration, and neither is derivable from the other. Some records legitimately exist in one language only, because that is all the source had. So neither language can be the master and the other a derived copy: both are real fields on the record, and either may be absent.

That propagates outward. Sorting a list differs by language and cannot be one stored sort key. Search has to look at both forms or it fails the user who typed the name the way they know it. A report has to pick a language and stay in it. And the layout mirrors rather than translates — direction is part of how the record is presented, not a stylesheet toggle.

Two first-class languagesPer-language sortingCross-script searchRTL layout mirroring
AngularASP.NETjQueryEntity FrameworkBilingual data modelCustody events

Billing and invoicing for clinical work: raising the claim, following what happens to it, and invoicing what is actually collectable.

An invoice is a demand. A claim is a request that a third party is entitled to refuse — and most refusals have nothing to do with medicine. They are a code, an eligibility date, a missing authorisation number. The work was done, the patient was treated, and the money simply does not arrive.

What makes it expensive is the clock. Every payer has its own rules and its own resubmission window, and when that window closes the money is gone permanently — not disputed, gone. So the system's real job is not producing claims. It is holding the state of every claim, carrying each rejection reason in a form you can group and count, and putting the ones whose appeal window is closing in front of somebody while they can still act.

The moment that data is shaped properly, a second thing becomes possible that nobody asked for: the rejections stop being individual annoyances and become a pattern. One recurring coding mistake, found once, is worth more than any amount of chasing.

The claim is modelled as a state, every payer response is written as its own entry rather than replacing the last one, and the rejection reason is stored as a code you can group by instead of a sentence somebody typed. The working queue is then derived: not all open claims, but the ones whose window is about to close, worst first.

submitted refusedreason code, not prose corrected resubmitted paid RESUBMISSION WINDOW — SET BY THE PAYER, NOT BY YOU past this line it is not disputed — it is gone every entry above is appended, so the reasons can be counted
.NETSQL ServerClaim lifecycleDenial reasonsSSRS reporting

A shop has a price list. A clinic does not. What may be billed for one procedure depends on which payer is covering it, under which plan, at which contracted rate, on the date it was performed — and then splits again into the part the payer owes and the part the patient owes.

So price is not an attribute of the service. It lives at the intersection of service × payer × date, and the date matters because contracts renew and rates are revised with effect from a day that has usually already passed by the time anyone is told.

Any system that bills from a price column on the procedure is correct only until the first contract renewal, and then wrong silently — which is the expensive kind of wrong, because nothing errors. It just invoices the wrong amount, is partly paid, and the difference is written off months later as bad debt by somebody who assumes it was a claims problem.

Payer contractsEffective-dated ratesPayer vs patient splitNo price column

Documents held as organisational records rather than as files in a folder.

A file is bytes on a disk. A document is those bytes plus who is allowed to see them, plus which version is the current one, plus provable evidence of what the superseded version said.

That last part is the one that gets skipped, and it is the one that matters. If a new version overwrites the old, the organisation loses its ability to show that an earlier decision was reasonable on the information available at the time. The decision then looks negligent in hindsight, and there is nothing left to argue with.

Access makes it harder still, because permission is not a setting applied once. It changes as a document moves through its process — drafted, reviewed, approved, circulated — and afterwards somebody will ask who saw it and when. A system that only knows the current permissions cannot answer that question at all.

.NETSQL ServerVersioningAccess controlAudit trail

A document store nobody can search is a shared folder with extra steps, so search is not a feature here — it is the reason the system exists. But search is also where access control is most often quietly broken.

A result list that says 3 more results you do not have permission to view has already told you something: that a document matching restructuring exists, and roughly when it was filed. The same leak happens through result counts, through pagination, through an autocomplete that suggests a title, and through an error that distinguishes not found from not allowed.

So permission has to be applied while the search runs, not as a filter over results that were already assembled — and to the user, a document they cannot see must be indistinguishable from one that does not exist. That constraint is easy to state and is the thing that decides how the index is built.

Permission-aware searchNo existence leaksMetadata at captureIndex design

Every document system is built to keep things, and then somebody points out that an organisation is also obliged to stop keeping some of them. Records have retention periods, and holding personal data past its period is not thrift — it is exposure.

Which puts two correct rules in direct conflict. Retention says destroy this on schedule. A legal hold says destroy nothing connected to this matter, including things already scheduled. Both are mandatory, and the hold has to win — but only for what it covers, and only while it lasts, and the fact that a document was held and why has to survive the document itself.

So disposal cannot be a cleanup job somebody runs. It is a decision the system makes, records, and can justify afterwards — including the deletions it refused to perform.

Retention schedulesLegal hold overridesJustifiable disposal

Workforce and HR administration in daily use: people, grades, attendance, leave.

A policy is agreed in March and applies from January. A grade is revised, and the approvals already given under the old grade must stay exactly as they were given. Almost nothing in HR is simply true — it is true as of a date, and often backdated after the fact.

Store people as current values and the system can tell you what it believes today. It cannot tell you what it believed in February, which is precisely the question asked the moment there is a dispute — and in HR, disputes are the normal case rather than the exception.

Leave accrual is the sharpest version of this. The rules differ by contract, change part-way through a year, and have to be re-applied without invalidating leave somebody has already taken and already been granted. Getting that wrong does not produce an error message; it produces an employee who is told, months later, that days they were given do not exist.

.NETEntity FrameworkEffective-dated recordsAccrual rulesSSRS

Attendance looks like the simplest module in HR and is reliably the one that generates the most argument, because the system is not recording a fact. It is recording a device reading, and then inferring a fact from it.

A missing punch is not an absence — it is a missing punch. Somebody badged in and left through a different door; the reader failed; the shift crossed midnight so the two halves of one working day landed on two dates; a public holiday was declared after the roster was published. Each of those is a normal Tuesday, and each produces a reading that is technically true and factually wrong.

So the raw captures and the derived attendance have to be two different things. A supervisor correcting a day must not overwrite what the device recorded, because the moment the two are the same column, the correction is indistinguishable from the measurement and nobody can tell later whether the day was attended or merely approved.

Raw capture kept separateDerived attendanceShifts over midnightCorrections as entries

Half of an HR system is requests moving up a chain: leave, overtime, an advance, a transfer. The chain is drawn from the org structure, and the org structure is the thing that changes most.

The approver resigns while three requests sit with them. Somebody moves department and their old team's pending items go with them by accident. A manager has to approve their own request because they are also the department head. Someone delegates while on leave, and the delegation has to end without unwinding what was decided under it.

Which means an approval cannot be a pointer to a live user record. It has to record who approved, in what capacity, under whose authority, at that moment — frozen. Otherwise a promotion two years later silently rewrites the history of decisions, and the sentence this was approved by the department head stops being checkable.

Frozen approval contextDelegation with an endOrg change vs historySelf-approval cases

Remittance flows — the same reconciliation discipline as earned-wage access, carried across a border.

The sender is debited in a system you control. The beneficiary is paid by a partner you do not. Between those two facts sits a rate that was fixed at a particular moment and has to be provable afterwards, and a compliance check that can hold a transfer after the money has already left.

So the authority cannot be any single response. A partner can succeed and fail to tell you; a timeout tells you nothing about whether the payout happened. Day-end reconciliation stops being a report that somebody reads and becomes the mechanism that decides what actually occurred — and every transfer has to stay re-checkable long after it looked settled.

The person at the other end is usually a family receiving money they are relying on that week. A transfer stuck in an unclear state is not a data-quality issue to them.

.NETSQL ServerReconciliationIdempotencyThird-party integration

The sender is told, before they pay, exactly how much the beneficiary will receive. That figure is a commitment, and it is made at a moment when the transfer has not happened yet.

Between the quote and the settlement the market moves, and the gap is not always seconds — a compliance review, a partner outage or a weekend can push it into days. By the time the payout clears, the rate that was promised and the rate that was obtained are two different numbers, and the difference has to land somewhere.

The engineering question is not which rate is correct. Both are. It is whether the business decided in advance who absorbs the gap and for how long a quote stays honourable — or whether it finds out at month end, from a reconciliation that does not balance, that it has been quietly paying the difference on every delayed transfer.

Quote validity windowRate at quote vs at settlementWho absorbs the gapProvable afterwards

Screening sounds binary and is not. Names arrive transliterated from other scripts, so matching against a sanctions or watch list is necessarily fuzzy — and because the cost of a miss is severe, the thresholds are set so that false positives are the normal case. Most hits are an ordinary person who shares a name.

That has two consequences a design has to swallow. A hit must be able to stop a transfer that is already in motion, which means the flow needs a real paused state rather than an error. And because a human then decides, the decision and its reason have to be recorded against that transfer permanently — the question asked in an audit is not whether the system screened, but why this transfer was released.

The lists also change under you, and the rules differ by corridor: the same customer can be perfectly fine sending to one country and blocked sending to another. So screening is not a gate at the entrance. It is a condition the transfer lives under until it settles.

Fuzzy name matchingPaused, not failedRecorded decisionsCorridor-specific rules

Payroll in daily use, where being a day late or a rupee wrong is both visible and personal.

Payroll runs to a date that does not move for anybody, and every person affected checks the result personally. That alone makes it unusual: most systems can be a little wrong for a while before anyone notices.

The genuinely hard part is corrections. An error found this month belongs to a payslip that was already issued, already filed, and possibly already used to support a tax return or a loan application. It cannot be fixed by rewriting it. It has to be carried forward as a named adjustment in the current run, which means the system is required to hold two truths at once — what was paid, and what should have been paid — and to show both without confusing the person reading it.

Tax slabs and contributions change on dates set by somebody else, and they apply from those dates whether or not the change arrived in time. So the calculation cannot be written as code that knows today's rates. It has to be written as rules that know which period they belong to.

.NETSQL ServerPeriod-dated rulesAdjustmentsRDLC payslips

A payslip looks like a sum. It is actually the output of a few dozen small rules — allowances, overtime, a loan instalment, an advance recovery, statutory contributions, tax, arrears from a backdated increment — each with its own base, its own ceiling, and its own conditions.

And they are not commutative. A deduction taken before tax produces a different net figure from the same deduction taken after it. An allowance that counts toward the contribution base changes a number three rules downstream. So the sequence is itself a business rule, as much as any of the individual calculations.

Written as code in the order somebody happened to think of them, every future policy change becomes a code change, a release and a risk — on a system that must run correctly on a fixed date every month. Written as ordered, dated rules with their own inputs, a policy change is data, and last year can still be recalculated the way last year's rules said.

Ordered rule engineDated policy, not codeBases and ceilingsArrears and recoveries

Every other system in this list is questioned by an auditor or a colleague. Payroll is questioned by the person whose money it is, who is standing at a desk, who is often upset, and who is entitled to an answer that day.

And the answer cannot be the system calculated it. It has to be: this allowance, at this rate, because your grade changed on this date; this deduction, because of this loan, with this many instalments left; this much tax, on this slab. Line by line, for a month that has already closed.

Which means a payroll run cannot store only its results. It has to store its workings — which rules fired, on what inputs, in what order, producing what — because recomputing it later with today's rules would give a different number and answer the wrong question entirely. The explanation is not a report built afterwards. It is an output of the run, saved at the time.

Stored workingsLine-level explanationClosed periods stay closedRDLC payslips
03

The stack, with evidence attached

Forty-nine entries. Click any one for what it actually means in my work. Instead of a proficiency percentage, each carries what backs it — production on a named project, working use, or knowledge. There are no scores here, because a score is an opinion wearing a number's clothes.

Which technology, on which system

SystemAngularjQueryASP.NETEFSQL ServerMicroservicesLaravel
Tilly POS · for sale●—●●●——
Earned-wage access · PayActiv●—●●●●—
Ministry of Culture & Art · AMAN●●●●●——
PLRA land records——●●●——
Order & inventory demo (below)——————●

Why this table has gaps. A dash means I did not use it there — not that it is missing by oversight. Only the five systems I can speak to in detail appear. If a row you need is absent, it is because I will not fill a cell to make a table look complete.

04

The bottom layer: running code

A working order and inventory system, deployed, open, no sign-in. Everything above this point is description. This you can click.

daydreamatelier8.alwaysdata.net
Laravel 13PHP 8.4SQLiteServer-renderedNo front-end frameworkResets 03:00 nightly

Most inventory demos let you do anything. This one will not dispatch an order if any line is short — and it refuses the whole dispatch rather than shipping part of it and leaving the rest in a state nobody owns.

That single rule is where the engineering is, because it has to hold while several tables change at once.

One action owns the operation. It checks first, refuses with a reason, and only then opens a transaction so the order status, the stock movements and the order history either all happen or none do.

// app/Actions/AdvanceOrder.php — the real file behind the demo
if ($next === 'Dispatched') {
    if ($short = $this->linesWithoutStock($order)) {
        return Outcome::refused(
            "{$order->reference} cannot be dispatched: not enough stock of "
            . implode(', ', $short) . '.'
        );
    }
}

DB::transaction(function () use ($order, $next) {
    $from = $order->status;
    if ($next === 'Dispatched') { $this->releaseStock($order); }
    $order->update(['status' => $next]);
    OrderEvent::create([... 'actor' => 'demo visitor']);
});

order_events and stock_movements are append-only — the same shape as the land-records pattern above. Outcome is a readonly class: a refusal is a value the caller must handle, not an exception thrown into the dark.

advance() any lineshort? Outcome::refused( reason ) — nothing written DB::transaction — all three, or none orders.status · stock_movements · order_events append only

What this demo is and is not. The data is seeded sample data and it resets nightly at 03:00 so it is always clean when somebody opens it. It is not a product and not a real company's books. It is there so a claim about how I build can be checked in a browser instead of taken on trust.

05

Start a conversation

A name and one way to reach you is enough. Everything else helps me answer properly the first time rather than with a list of questions.