Ejan
Shrestha
I modernise legacy .NET and keep an insurance platform of 50 projects boring, on purpose.
Solution architectureinsurance platform on Kubernetes
Three replicas per service with autoscaling, Postgres read replicas, Redis, and RabbitMQ with an outbox and a saga. Tap a pod to kill it, or replay a transaction.
- .NET projects
- 50
- Kubernetes deployments
- 28
- sockets leak fixed
- ~390 call sites
- target runtime
- .NET 10
- C#
- ASP.NET Core
- EF Core
- Dapper
- Hangfire
- YARP
- Refit
- IdentityServer
- PostgreSQL
- SQL Server
- Redis
- Query tuning
- Migrations
- Docker
- Kubernetes
- Nginx
- Traefik
- Linux
- Let's Encrypt
- Serilog
- Seq
- Grafana Loki
- Correlation IDs
- Next.js
- React
- TypeScript
- Tailwind
- Strapi
Problems I was trusted with.
Written the way I'd brief a new engineer: what broke, what I changed, what it bought us.
case 01 / 05
~390
new HttpClient() call sites retired
- .NET
- HTTP
- Reliability
Ending socket exhaustion across the platform
- Problem
- Under load, outbound calls failed with address-already-in-use errors and timeouts cascaded between services.
- What I did
- Traced it to roughly 390
new HttpClient()call sites stuck in TIME_WAIT, then moved them to IHttpClientFactory with named and typed clients and explicit timeouts. - Result
- The error class disappeared from production logs, and outbound HTTP now has one place for retries and tracing.
case 02 / 05
1 ID
follows a request across every service
- Observability
- Serilog
- Loki
One trace from gateway to database
- Problem
- Every service logged differently, and services on .NET Core 2.2 logged almost nothing useful.
- What I did
- Built a shared logging library (Serilog, Seq, Grafana Loki, global exception middleware) plus a mirror for legacy services so both generations emit the same shape.
- Result
- One correlation ID now follows a request end to end.
case 03 / 05
61%
of projects were on unsupported .NET
- Architecture
- Migration
From end-of-life .NET to .NET 10
- Problem
- Most projects targeted .NET Core 2.2, 3.1 or 5, and nobody knew which ones blocked the rest.
- What I did
- Audited every project, identified IdentityServer4 as the critical blocker, and planned a direct path to .NET 10.
- Result
- A sequenced roadmap with known risks; the first services already run on .NET 10.
case 04 / 05
v16
Next.js, with React 19, Strapi 5 and Node 24 alongside
- Next.js
- Strapi
- Release
A public website on Next.js 16
- Problem
- The site and CMS sat on unsupported versions with known vulnerabilities.
- What I did
- Upgraded the front end, CMS and mail service together, with a rollback kit prepared before going live.
- Result
- Shipped to production with a tested rollback path.
case 05 / 05
4
permanent branches, one promotion path
- Process
- Git
- Leadership
A release standard people follow
- Problem
- Hotfixes reached prod without reaching dev, and UAT drifted from what shipped.
- What I did
- Wrote and rolled out permanent main, dev, uat and prod branches with a clear promotion path.
- Result
- Releases are predictable enough to schedule.
184 milliseconds, seven layers.
Every request crosses a proxy, a gateway, a service, an HTTP client, a database and a logger. I've broken and fixed each of them, and wrote up what I learned. Pick a span.
POST/api/policies/quote
trace 7f3a…c91e, 200 OK
0ms
One request through the stack I run. Every span is a layer I've built, broken or fixed. Pick one to read the article about it.
From the controller to architecture, and back.
One feature, issuing a policy, written the way engineers write it from their first year to the architect's chair. Scroll through it.
01/07Junior
Make it work.
Everything lives in the controller: SQL, business rules, email, all of it. It works on my machine and nobody knows why.
Controllers/PolicyController.cs[HttpPost("save")] public IActionResult Save(PolicyDto dto) { var conn = new SqlConnection("Server=prod;User=sa;Password=123"); conn.Open(); var sql = "INSERT INTO Policy VALUES ('" + dto.Name + "', " + dto.Premium + ")"; new SqlCommand(sql, conn).ExecuteNonQuery(); if (dto.Premium > 100000) new SmtpClient("mail").Send("boss@co", "Big policy!", dto.Name, ""); return Ok("saved"); // works on my machine }stage 01/0702/07Mid-level
Make it layered.
Controllers get thin, a service layer appears and the first unit tests show up. Mostly checking that mocks return mocks.
Services/PolicyService.cspublic class PolicyService : IPolicyService { private readonly IPolicyRepository _repo; private readonly IEmailService _email; public PolicyService(IPolicyRepository repo, IEmailService email) => (_repo, _email) = (repo, email); public async Task<int> CreateAsync(PolicyDto dto) { var id = await _repo.AddAsync(dto.ToEntity()); if (dto.Premium > 100_000) await _email.NotifyUnderwriterAsync(id); return id; } }stage 02/0703/07Senior
Make it right.
Red, green, refactor. The test comes first, and SOLID stops being an interview answer and starts shaping the code.
SOLID, with insurance examples
Single responsibility
One reason to change. Pricing knows nothing about email.
class PremiumCalculator { Money Calculate(Risk risk) { /* … */ } } class UnderwriterNotifier { Task Notify(PolicyId id) { /* … */ } }Tests/IssuePolicyTests.cs// Red: written before Policy.Issue exists [Fact] public void Issuing_above_the_limit_needs_underwriting() { var policy = Policy.Quote(Premium.Of(250_000m, "NPR")); var result = policy.Issue(Limits.Default); result.Should().BeOfType<NeedsUnderwriting>(); policy.Status.Should().Be(PolicyStatus.Referred); } // Green: the smallest Issue() that passes. // Refactor: now the rest of the design can move safely.stage 03/0704/07Tech lead
Make it last.
Clean architecture inside each module, a modular monolith around them. Boundaries get agreed in ADRs before they get argued in code review.
Dependencies point inward. The domain knows nothing about EF Core or HTTP. Pick a ring to see its code.
Domain/Policy.cs
public sealed class Policy : AggregateRoot { public PolicyStatus Status { get; private set; } = PolicyStatus.Quoted; public Money Premium { get; private set; } public Result Issue(Limits limits) { if (Premium > limits.AutoIssue) return Refer("needs underwriting"); Status = PolicyStatus.Issued; Raise(new PolicyIssued(Id, Premium)); return Result.Ok(); } }stage 04/0705/07Architect
Make it scale.
Modules become services when a team or a load profile demands it, not before. Messages over RabbitMQ, an outbox for reliability, a saga instead of a distributed transaction.
Reinsurance/PolicyIssuedConsumer.cspublic sealed class PolicyIssuedConsumer(ITreaties treaties, IPublishEndpoint bus) : IConsumer<PolicyIssued> { public async Task Consume(ConsumeContext<PolicyIssued> ctx) { var msg = ctx.Message; var cession = await treaties.Cede(msg.PolicyId, msg.Premium); // No distributed lock: each service commits locally, the saga reacts await (cession.IsRejected ? bus.Publish(new CessionRejected(msg.PolicyId)) : bus.Publish(new PolicyCeded(msg.PolicyId, cession.Share))); } }stage 05/0706/07Solution architect
It depends.
Every answer starts with a trade-off table. The solution architecture at the top of this page is the job: every box a decision, every arrow a trade-off.
docs/adr/0007-cross-service-writes.md# ADR 0007: Cross-service writes Status: accepted | Option | Consistency | Ops cost | Team fit | |----------------------|-------------|----------|----------| | Distributed tx (2PC) | strong | high | poor | | Outbox + saga | eventual | medium | good | | Shared database | strong | low | coupling | Decision: outbox + saga over RabbitMQ. Consequence: every consumer is idempotent (Redis keys).stage 06/0707/07Friday, 4:55 pm
And then the deadline.
Ten years of TDD, DDD, clean architecture and microservices later, the hotfix ships in the controller. Again.
Controllers/PolicyController.csworks on prod[HttpPost("hotfix")] public async Task<IActionResult> Fix() { // TODO: move this into the domain after the release await _db.Database.ExecuteSqlRawAsync( "UPDATE Policy SET Status = 'Issued' WHERE Id = 4021"); return Ok("fixed"); // it's Friday }stage 07/07
[HttpPost("save")]
public IActionResult Save(PolicyDto dto)
{
var conn = new SqlConnection("Server=prod;User=sa;Password=123");
conn.Open();
var sql = "INSERT INTO Policy VALUES ('" + dto.Name + "', " + dto.Premium + ")";
new SqlCommand(sql, conn).ExecuteNonQuery();
if (dto.Premium > 100000)
new SmtpClient("mail").Send("boss@co", "Big policy!", dto.Name, "");
return Ok("saved"); // works on my machine
}From data desk to platform owner.
BSc Computer Science and Information Technology
Vedas College, 2017 – 2022
Jan 2025 – presentnow
Senior Software Engineer
Himalayan Everest Insurance, Kathmandu, hybrid
- Own backend reliability and modernisation for an insurance platform of 50 .NET projects, deployed as 28 Kubernetes workloads, across accounting, claims, reinsurance and reporting.
- Leading the upgrade from .NET Core 2.2, 3.1 and 5 to .NET 10, including the IdentityServer4 replacement.
- Built structured logging and tracing across every service.
- Upgraded the public website and CMS to Next.js 16, React 19, Strapi 5 and Node 24.
Jul 2022 – Nov 2024
.NET Developer
EKbana Solutions, Lalitpur, on-site
- Designed backend services for enterprise insurance workflows with ASP.NET Core, PostgreSQL, EF Core and Dapper.
- Built REST APIs and integrations for customer portals, payments and external partners.
- Ran background workflows on Hangfire with idempotency, retries and failure recovery.
- Shipped on Docker, Linux, Nginx/Traefik and Kubernetes; worked on IdentityServer, JWT and RBAC.
Jan 2022 – Jun 2022
Dotnet Developer
NBIT, Lalitpur
- Built and maintained .NET Framework applications for client projects.
Nov 2017 – Jun 2022
Data Specialist
CloudFactory, Kathmandu
- Delivered high-accuracy data work for international clients while completing my degree.
Notes from production.
14 articles on .NET, infrastructure and running a team. Most started as an incident.
Got a system that needs a steady hand?
Open to senior and lead roles in backend architecture and .NET modernisation, in Kathmandu or remote.
Local time in Kathmandu: KTM, UTC+5:45
➜ ~ whoami
Ejan Shrestha, Senior Software Engineer, Kathmandu, Nepal
➜ ~ contact --email
➜ ~