Skip to content
← All case studies

05 — IICT, BUET

Gas Utility ERP

One system, three state utilities, and the full life of a gas connection

2019 — 2022Assistant Programmer → Programmer
Deployments
Three state utilities
Surface
~90 controllers, 15 modules
Tenure
3 years 8 months
Core
Grails · Oracle · PL/SQL

The problem

Three state-owned gas distribution companies needed the software that runs their business: every customer, every meter, every bill, and every dispute.

This was my first real system, and it was a large one. Not large in traffic — large in surface. Around ninety controllers across fifteen modules, because a utility’s domain genuinely is that broad, and every part of it maps to something a person in an office does every day.

Shape of the system

gas utility erp
DEPLOYMENTS · ONE CODEBASEUtility Adeployment 1Utility Bdeployment 2Utility Cdeployment 3Grails monolith~90 controllers · 15 modules · GroovyCONNECTION LIFECYCLEmodelled with history, not a flagdemand note → seal → dispute → reconnectCustomerMeteringBillingRevenueVigilanceLegal / suitsStore · HRReportsOraclePL/SQL billingset-shaped work,next to the dataSpring Boot REST APIJWT · Swagger · Spring Dataadded alongside, not insideWeb portalMobile clientJasper Reports — the actual product, for operators
One codebase, three utility deployments. The connection lifecycle in the centre is what every other module asks questions of — which is why it is modelled with history rather than a status flag.

A Grails monolith over Oracle, with billing computation pushed down into PL/SQL and reporting through Jasper. Later, a Spring Boot REST layer alongside it — JWT-secured and Swagger-documented — so portals and mobile clients could arrive without disturbing the system that was already generating bills.

What a gas connection actually involves

The part I underestimated at twenty-three: the interesting complexity was not technical.

A connection is created, given appliances with rated capacities, metered, billed on a tariff that depends on customer class, and read on a serial schedule. Then reality arrives. The customer changes their appliance load. The property changes owner. The meter is replaced. The customer does not pay, so a demand note is issued, then the connection is disconnected and sealed. They dispute it. It goes to court, and now there is a case with a lawyer, a court, a panel area and hearing dates — all of which the system tracks, because the utility’s legal team works out of the same platform as its billing team.

Modelling that as a lifecycle with history rather than a status flag is the single design choice the whole system rests on. Every downstream module — vigilance, legal, revenue reporting — needs to answer how did this connection get into this state, and who authorised each step. A flag throws that away the moment it is overwritten.

Why the monolith was right

It would be easy, writing this now, to say it should have been services. It should not have been.

One team, one database, one deployment per utility, and a domain where nearly every module reads the customer. Splitting that would have bought distributed transactions and network calls in exchange for nothing anyone needed. The monolith was legible: you could follow a bill from controller to stored procedure in one repository.

What I would change is the boundary discipline inside it — clearer module seams, so the parts that did eventually want to be separate could leave cleanly.

What it taught me

Three things I still work by:

  • Domain complexity outweighs technical complexity. Nothing here was hard to build. Getting the rules right — whose tariff, whose liability, which surcharge — was hard, and it was the job.
  • Software for state utilities is long-lived. Code written in 2019 was still billing customers years later. That changes how you write it.
  • Reports are not a feature, they are the product for the people who actually use it. The operators’ day is spent in Jasper output, not in the screens I found interesting.

Decisions & trade-offs

Every choice below had a credible alternative. These are the ones I turned down, and why.

  1. 01

    choseOne codebase deployed per utility, not three forks

    notCopying the system for each company and letting each diverge

    Three utilities share about ninety percent of their domain — a meter is a meter, a tariff is a tariff. Forking would have tripled the cost of every fix. Keeping one codebase with per-deployment configuration meant a correction to billing logic reached all three.

  2. 02

    choseStored procedures for billing calculation, application code for workflow

    notMoving all calculation into the application tier

    Bill computation is set-shaped work over large tables, and it lives next to the data for good reason. Workflow and state transitions are branching logic that belongs in code where it can be read and tested. Splitting on that line played to each tier's strength.

  3. 03

    choseAdd a REST API layer rather than extend the Grails monolith

    notServing new portal and mobile clients from the existing server-rendered app

    The monolith was correct and working — the wrong thing to destabilise. A separate Spring Boot API with JWT and Swagger let new clients arrive without touching the system that was already billing customers.

  4. 04

    choseModel disconnection and reconnection as an explicit lifecycle

    notA boolean status flag on the customer record

    A connection does not have two states. It gets a demand note, gets sealed, gets disputed, gets reconnected, migrates zone, changes owner. A flag cannot answer 'why is this customer disconnected and who authorised it' — a modelled lifecycle with history can, and the vigilance and legal modules depend on that answer.

Stack

GroovyGrailsJavaSpring BootOraclePL/SQLJasper ReportsJWTSwagger
Next case study

Invoice Generation Pipeline

Event-driven postpaid billing at national scale