Community Article
Community articles are authored by SitePoint Premium contributors. Content is screened before publication, and SitePoint reserves the right to moderate or remove articles that violate our guidelines. Views expressed are those of the authors and do not necessarily reflect those of SitePoint.
How Developer Can Build More Reliable Software for Regulated Industries
SASaifullah AdenwallaPublished inDeveloper Tools·
August 28, 2026
·Updated:August 28, 2026
The AI briefing for Developers
Stay up to date with AI tools, model releases, and developer workflows that matter.
Weekly. Free. One click to leave.
SitePoint Premium
Stay Relevant and Grow Your Career in Tech
- Premium Results
- Publish articles on SitePoint
- Daily curated jobs
- Learning Paths
- Discounts to dev tools
7 Day Free Trial. Cancel Anytime.
A bug in a photo-sharing app might result in a broken thumbnail.
A similar failure in financial, healthcare, insurance, or compliance software can have very different consequences. A duplicated transaction, an incorrectly authorized record, a missing audit event, or a failed data export can become more than an inconvenience.
That changes the meaning of “reliable software.”
In most applications, reliability means that the system behaves correctly and recovers gracefully when something fails. In regulated environments, developers usually need something else as well:
the ability to demonstrate what the system did.
It should be possible to answer questions such as:
Which version was running?
Who performed an important action?
Which permissions were evaluated?
Was the request processed once or twice?
Which configuration was active?
Can a failed operation be reconstructed?
Can a backup actually be restored?
This doesn’t require filling the codebase with compliance-specific abstractions. Most of the useful techniques are familiar engineering practices: validation, state machines, idempotency, authorization, structured logging, automated testing, reproducible builds, and tested recovery procedures.
The difference is that we need to apply them deliberately.
Let’s look at how those pieces fit together in a Node.js application.
Turn Requirements Into Observable Behavior
Regulatory requirements often arrive as broad statements.
Only authorized employees may approve transactions.
Important account changes must be traceable.
Those requirements make sense to people.
They’re not specific enough for a test suite.
Developers need to translate them into observable behavior.
Only authorized employees can approve transactions.A user without the transactions:approve permissionreceives HTTP 403 when requesting approval.Now we can test it.
importrequestfrom"supertest";import{ app }from"./app.js";test("rejects transaction approval without permission",async()=>{const response =awaitrequest(app).post("/transactions/tx-81/approve").set("Authorization","Bearer regular-user-token");expect(response.status).toBe(403);});Do the same with audit requirements.
Account modifications must be traceable.Every successful account email change createsan audit event containing:actor IDaccount IDactionrequest IDtimestampresultThis transformation from policy language into measurable system behavior is one of the most important engineering skills in regulated projects.
For developers unfamiliar with the domain, looking at the kinds of workflows commonly handled in RegTech software development can also help turn an abstract idea such as “compliance software” into concrete engineering problems such as identity verification, monitoring, reporting, audit trails, risk workflows, and regulatory-data management.
How do we make this application compliant?
That’s too broad.
Which behaviors must always be true?
Those are much easier to implement and test.
Validate Data Before It Reaches Business Logic
JavaScript makes it easy to accept flexible objects.
That flexibility becomes dangerous when application logic silently assumes that incoming data is valid.
app.post("/api/payments",async(req, res)=>{const payment =awaitcreatePayment(req.body);res.json(payment);});What happens if the request contains:
{"accountId":"","amount":-400,"currency":"hello"}The deeper malformed data travels through the system, the harder failures become to diagnose.
Create a clear validation boundary.
Using a schema library, the conceptual structure might be:
const paymentSchema ={accountId:"required-string",amount:"positive-number",currency:["USD","EUR","GBP"]};app.post("/api/payments",async(req, res)=>{const result =validatePayment(req.body);if(!result.valid){return res.status(400).json({errors:result.errors});}const payment =awaitcreatePayment(result.value);res.status(201).json(payment);});Think of the architecture like this:
Untrusted input↓Validation↓Trusted application boundary↓Business logicValidation doesn’t make software compliant by itself.
It does make important assumptions explicit.
That’s valuable in any application and particularly useful when data integrity matters.
SitePoint’s guide to forms, file uploads, and security with Node.js and Express applies the same broader principle: don’t treat client input as trusted merely because it reached your server.
Make Important State Explicit
Boolean-heavy state is surprisingly good at representing impossible situations.
Imagine a claim object like this:
{approved:true,rejected:true,closed:false,underReview:true}What does that mean?
The software allows several independent flags to represent something the business probably considers one state.
constCLAIM_STATES={DRAFT:"draft",SUBMITTED:"submitted",UNDER_REVIEW:"under-review",APPROVED:"approved",REJECTED:"rejected",CLOSED:"closed"};Then describe allowed transitions:
const transitions ={draft:["submitted"],submitted:["under-review"],"under-review":["approved","rejected"],approved:["closed"],rejected:["closed"],closed:[]};Validation becomes deterministic:
functioncanTransition(current,next){return(transitions[current]?.includes(next)??false);}functiontransitionClaim(claim,next){if(!canTransition(claim.status,next)){thrownewError(`Invalid state transition:`+`${claim.status}→${next}`);}return{...claim,status: next};}This architecture provides another advantage.
Authorization and audit requirements can be associated with transitions.
under-review → approvedpermission:claims:approveaudit event:claim.approvedThe business workflow becomes visible in code instead of emerging accidentally from several unrelated flags.
Make Retries Safe
Network requests occupy an uncomfortable space.
A client sends a request.
The server processes it.
The response is lost.
The client sees a timeout.
Did the operation happen?
POST/api/paymentsRetrying blindly may create two payments.
One common solution is an idempotency key.
Idempotency-Key: 8d62bc18-...The server associates that identifier with the operation.
asyncfunctioncreatePayment({input,idempotencyKey}){const existing =await paymentRepository.findByIdempotencyKey(idempotencyKey);if(existing){return existing;}return database.transaction(asynctx=>{return tx.payments.create({...input,idempotencyKey});});}Application-level checking is useful, but a database uniqueness constraint provides stronger protection against concurrent requests.
CREATEUNIQUEINDEXpayments_idempotency_keyON payments(idempotency_key);Now two requests racing through separate application processes can’t silently create two records using the same key.
Idempotency isn’t only useful for money.
It can protect operations such as:
creating a claimsubmitting a reportissuing a documentregistering a customerapproving an applicationcreating an external jobWhenever clients may retry an important state-changing request, ask:
What happens if the server receives this twice?
Match Database Transactions to Business Transactions
awaitdebitAccount(source,amount);awaitcreditAccount(destination,amount);If the first operation succeeds and the second fails, the database now represents a business state that should never have existed.
If both writes represent one logical action, use a database transaction where appropriate.
await database.transaction(asynctx=>{await tx.accounts.debit(source,amount);await tx.accounts.credit(destination,amount);await tx.transfers.create({source,destination,amount});});The exact transaction model depends on the database and architecture.
technical consistency boundaries should reflect business consistency boundaries.
Distributed systems make this more difficult because one business operation may cross several services.
But even then, defining the desired state transitions explicitly helps the team choose appropriate strategies around queues, compensating operations, outboxes, retries, and reconciliation.
Treat Authorization as Server-Side Business Logic
{user.isAdmin&&<button>Delete record</button>}It’s useful.
It isn’t authorization.
DELETE/api/records/418The server must decide whether that action is allowed.
functionrequirePermission(permission){return(req,res,next)=>{if(!req.user.permissions.includes(permission)){return res.status(403).json({error:"Forbidden"});}next();};}app.delete("/api/records/:id",requirePermission("records:delete"),deleteRecord);This becomes even more important when users have limited scope.
Suppose an insurance employee may access only records belonging to their business unit.
const claim =await claims.findById(req.params.id);and rely entirely on a later UI check.
Where possible, scope the data query itself:
const claim =await claims.findOne({id:req.params.id,businessUnitId:req.user.businessUnitId});The application should make unauthorized data difficult to retrieve, not merely difficult to display.
SitePoint’s recent tutorial on building a secure employee document portal with Node.js and Express explores this distinction in more depth, particularly the difference between authentication, re
Build Audit Logging Separately From Debug Logging
Developers already create logs.
That doesn’t automatically create a useful audit trail.
logger.error({message:"Database query failed",duration:3400});helps engineers diagnose a technical problem.
An audit event answers a different question:
Which actor performed which important action?
{event:"customer.email.changed",actorId:"user-829",resourceId:"customer-481",requestId:"req-729a",outcome:"success",timestamp:"2026-08-28T09:14:03Z"}Treat this as its own concern.
asyncfunctionaudit({event,actorId,resourceId,requestId,outcome}){await auditStore.append({event,actorId,resourceId,requestId,outcome,timestamp:newDate().toISOString()});}Then sensitive workflows call it deliberately:
await customerRepository.updateEmail(customerId,email);awaitaudit({event:"customer.email.changed",actorId:req.user.id,resourceId:customerId,requestId:req.id,outcome:"success"});There is another important rule:
don’t turn audit storage into a duplicate database of sensitive information.
passwordsaccess tokensfull payment detailsprivate document contentssecret keysunnecessary personal datainto audit events.
An audit trail should tell you what happened without creating another sensitive-data problem.
OWASP similarly recommends consistent application-level security logging and warns that application logging needs deliberate design rather than relying only on infrastructure logs.
Give Requests Correlation IDs
A single user action may pass through:
Gateway↓Authentication↓Application API↓Queue↓Worker↓DatabaseWithout a shared identifier, reconstructing a failure may require searching several systems by timestamp and guessing which events belong together.
Give requests an identity.
importcryptofrom"node:crypto";functionrequestId(req,res,next){req.id=req.get("x-request-id")||crypto.randomUUID();res.set("x-request-id",req.id);next();}Now include it in important events.
logger.info({event:"report.started",reportId,requestId:req.id});await queue.send({type:"generate-report",reportId,requestId:req.id});The worker keeps the same identifier:
logger.info({event:"report.worker.started",reportId:job.reportId,requestId:job.requestId});Now an incident can be reconstructed around:
requestId = 5b8429...rather than a collection of loosely related timestamps.
Treat Configuration as Executable Behavior
The software running in production isn’t just your JavaScript.
Code+Configuration+Infrastructure+DataREQUIRE_SECOND_APPROVAL=falseA perfectly implemented approval flow can behave incorrectly if production receives the wrong configuration.
Validate important assumptions during startup.
functionvalidateConfig(env){const required =["DATABASE_URL","AUDIT_STORE_URL","TOKEN_ISSUER"];for(const name of required){if(!env[name]){thrownewError(`Missing configuration:${name}`);}}if(env.NODE_ENV==="production"&&env.REQUIRE_SECOND_APPROVAL!=="true"){thrownewError("Second approval must be enabled in production");}}Call it before accepting requests.
validateConfig(process.env);app.listen(3000);Failing at startup is much safer than silently operating under an invalid assumption.
Configuration changes also deserve:
version controlcode reviewtestingdeployment historyrather than being treated as anonymous edits made manually on a server.
Test Failure Paths Deliberately
A test suite can contain thousands of assertions and still provide false confidence if nearly every test demonstrates that valid input works.
Reliable systems need tests for uncomfortable scenarios.
the database times outan external API returns 503a queue delivers a message twicetwo approvals arrive simultaneouslyauthentication expires mid-requesta webhook is delivered twicestorage becomes unavailablea process crashes during workSuppose an external request is retryable.
A basic retry utility might look like:
asyncfunctionretry(operation,{attempts =3,baseDelay =200}={}){let lastError;for(let attempt =1;attempt <= attempts;attempt++){try{returnawaitoperation();}catch(error){lastError = error;if(attempt === attempts){break;}awaitnewPromise(resolve=>setTimeout(resolve,baseDelay *attempt));}}throw lastError;}But don’t automatically retry everything.
503 Service Unavailablemay be retryable.
400 Invalid requestprobably isn’t.
401 Unauthorizedmay require refreshing credentials rather than retrying blindly.
And retrying a non-idempotent operation can make a problem worse.
Reliability doesn’t mean “retry on error.”
It means understanding what each failure means.
Build CI as an Evidence-Producing Pipeline
Continuous integration is often described as:
Run tests↓Get green checkFor higher-assurance systems, it can provide much more useful evidence.
A deployment pipeline might be:
Pull request↓Lint↓Type checks↓Unit tests↓Integration tests↓Security checks↓Migration verification↓Build immutable artifact↓Approval↓DeploymentThe important property is that the artifact being tested should be closely related—or ideally identical—to the artifact eventually deployed.
{"version":"6.4.1","commit":"e27ad81","buildId":"build-9182","createdAt":"2026-08-28T09:40:00Z"}Make it available operationally.
app.get("/version",(req, res)=>{res.json(buildMetadata);});During an incident, the team should be able to answer:
Which build is running?
without SSHing into a machine and guessing.
The same principle applies to:
database migrationsconfiguration versionsdependency manifestscontainer imagesinfrastructure changesReproducibility reduces uncertainty.
Design Recovery Before Production Needs It
Backups are not recovery plans.
A backup becomes useful only when somebody can restore it successfully.
A meaningful recovery test should answer:
Can the backup be restored?How long does restoration take?Are encryption keys available?Which release does the backup correspond to?Are permissions correct after restoration?Who is allowed to perform recovery?Can someone other than the original author follow the procedure?You can automate part of this.
For example, a scheduled recovery test might:
Create temporary environment↓Restore latest backup↓Run integrity checks↓Verify record counts↓Run smoke tests↓Destroy temporary environmentbackup completed successfullysystem can be recovered successfullyThose are different claims.
Some organizations also maintain encrypted offline copies of approved recovery material, release artifacts, configuration exports, or incident evidence. If a Mac-based engineering team uses removable media for an authorized recovery workflow, a practical reference explaining the best format for Mac external drive can help determine whether a Mac-focused filesystem or a cross-platform option better matches that workflow.
The filesystem choice itself isn’t the recovery strategy.
The engineering lesson is that a recovery document should be specific enough to execute.
Copy backups to external storage.isn’t enough.
A stronger procedure specifies:
what is copiedhow it is encryptedwhere it is storedwhich filesystem/platform is expectedhow integrity is verifiedwho has accesshow restoration is testedReliability lives in those details.
Keep Changes Small Enough to Understand
Imagine one deployment changes:
authenticationauthorizationdatabase schemaaudit loggingpayment processingfrontend routingcloud permissionsProduction begins failing.
Where do you look first?
Smaller changes reduce uncertainty.
Feature flags can also separate deployment from activation.
if(features.newApprovalFlow){returnnewApprovalFlow(request);}returnexistingApprovalFlow(request);But flags need lifecycle management too.
A useful flag record might include:
{name:"newApprovalFlow",owner:"risk-platform",createdAt:"2026-08-10",removeAfter:"2026-09-15"}Otherwise temporary infrastructure becomes permanent complexity.
Every flag creates another possible system state.
Keep them deliberate.
Design for Observability Without Collecting Everything
Developers need enough information to understand production behavior.
That doesn’t mean recording every piece of available data.
Structured operational events are usually more useful than enormous string logs.
console.log("payment stuff happened");logger.info({event:"payment.processing.started",paymentId,requestId:req.id});logger.info({event:"payment.processing.completed",paymentId,requestId:req.id,durationMs:218});logger.error({event:"payment.processing.failed",paymentId,requestId:req.id,reason:"provider-timeout"});searchaggregatecorrelatealert onanalyzewithout automatically copying the complete request body into every record.
Good observability is selective.
Make Important Operations Boring
This may be the best reliability goal for regulated systems.
An approval should be boring.
A deployment should be boring.
A restore should be boring.
A permission change should be boring.
which code path runswhich permission is checkedwhich transaction changeswhich audit event is generatedwhich build performed ithow to reverse or recover itSurprises are useful during experiments.
They’re expensive in production.
A system becomes easier to trust when important operations follow explicit, repeatable paths.
Reliability Is an Organizational Property Too
Sometimes an application appears reliable because one experienced engineer knows how to fix everything.
When production fails, everybody calls that person.
which server matterswhich database query to runwhich configuration is unusualwhere the backup liveshow to repair the queueThat’s expertise.
It isn’t resilience.
Move important knowledge into:
testsrunbooksstructured logsversioned configurationdashboardsarchitecture decisionsdeployment historyrecovery proceduresA developer responding to an incident should be able to answer:
What changed?Which build is running?What failed?Which requests were affected?Can the operation be retried?Is the data still consistent?How do we roll back?How do we verify recovery?without depending entirely on someone’s memory.
Secure Development and Reliable Development Overlap
Many practices associated with regulated systems are simply characteristics of well-engineered software.
NIST’s Secure Software Development Framework, for example, recommends integrating secure development practices into the software development lifecycle rather than treating security as a final step before release.
That idea maps naturally to the architecture we’ve discussed.
Don’t wait until the end of development to ask:
Can we audit this action?Is this request idempotent?Can we restore the data?Which users can access this record?Which configuration was deployed?Can we prove which build produced this behavior?Those questions are cheaper to answer when they’re part of the design.
The best outcome is not a separate “compliance layer” sitting on top of the application.
It’s an application whose normal engineering workflow naturally produces the controls and evidence the organization needs.
Final Thoughts
Building software for regulated industries doesn’t require every function to become complicated.
It does require developers to manage uncertainty carefully.
That means making important behavior explicit.
Validate data at trust boundaries.
Model business state deliberately.
Make retries safe.
Use transactions where business consistency requires them.
Enforce authorization on the server.
Separate audit trails from debugging logs.
Give requests stable identifiers.
Validate critical configuration.
Test failure paths.
Build reproducible artifacts.
And practice recovery before an incident forces you to.
None of those techniques belongs exclusively to financial services, healthcare, insurance, or regulatory technology.
They’re good software-engineering practices.
The difference is that in a regulated environment, the question isn’t only:
Does the system work?
Sooner or later, somebody may also ask:
Can you show us exactly how you know?
A reliable architecture should be able to answer both.


