Portfolio Manifest — Updated September 20, 2026
Principal Full-Stack Engineer
GridPulse is an original, from-scratch event-driven platform simulating a utility
company's meter-reading, usage-aggregation, and billing pipeline — not a fork or a cloned reference
app. Built to demonstrate the skills a Principal Developer role expects: distributed systems design,
microservices decomposition, event streaming, polyglot service boundaries, and production-grade
observability, tracked in the open from before the first service existed. Phases 1 through 3 are
complete and verified live end-to-end: the core Kafka-driven loop, a real Account/Customer Service
backing genuine AccountId-keyed usage and billing, and a real Node.js/TypeScript
Notification Service delivering an actual webhook POST. Three features shipped since: a real Green
Button/NAESB ESPI usage-data export, a real Stripe test-mode payment integration, and a Grid
Operations service correlating real outages and work orders from meters that stop transmitting —
102 automated tests, six services, one real distributed system. Phase 4a — a Grid Operations
console for field crews and dispatchers — is functionally complete and verified live end-to-end:
Duende IdentityServer, its BFF gateway, and the React/Redux Toolkit frontend (OIDC login,
role-based routing, a Work Orders queue, an Outages view, a map-location feature) are all built
and working; automated tests and remaining UI polish are still outstanding.
The architecture was written down before a line of service code was
The domain — utility metering and billing — is deliberately chosen, but the pipeline shape generalizes to any high-volume telemetry-to-billing system: IoT sensor networks, subscription usage billing, order processing. The full architecture, service breakdown, Kafka event contracts, and cross-cutting concerns (resiliency, idempotency, tracing, security, testing) were designed and documented up front, before any of it was built.
Six phases, staged deliberately — the core domain loop first, Kafka and the dashboard later
Phase 1 — a direct-REST core loop (Meter Simulator → Usage Aggregation → Billing) on a single
Postgres database — shipped first. Phase 2 replaced those REST calls with real Kafka events through
a real Confluent Schema Registry. Phase 3 added a genuine Account/Customer Service, re-keyed usage
and billing from a placeholder MeterId to a real AccountId, and shipped
this project's first polyglot service — a Node.js/TypeScript Notification Service delivering an
actual webhook POST. Three features shipped after Phase 3, ahead of the dashboard: a real Green
Button/NAESB ESPI usage-data export, a real Stripe test-mode payment integration closing the loop
from usage to billing to actual payment collection, and a Grid Operations service correlating real
outages and work orders from meters that stop transmitting. All of it verified live end-to-end, 102
tests passing. Phase 4a — a Grid Operations console for field crews and dispatchers — is
functionally complete and live-verified: Duende IdentityServer, its BFF gateway, and the
React/Redux Toolkit frontend are all built and working (automated tests and UI polish still
outstanding), and the customer billing dashboard, CI/CD, and full observability follow after —
each scaffolded one file or folder at a step with nothing batched ahead of review.
Independent scaling of ingestion vs. billing, a natural audit trail of events, and the ability to add new consumers later without touching the producers — reasoning recorded in the design doc, not asserted after the fact.
A wiki, a GitHub project board (Backlog/Planned/In Progress/Verification & QA/Done), and this working log all went live during Phase 1's scaffolding — not retrofitted once there was something to show.
All logged in todo.md, checkable against the commit history
From CodeQL failing before any service existed, to a live DNS resolution bug caught only by running the real distributed app end-to-end, to a Node.js Kafka consumer crashing permanently on a poison message only real traffic could surface — every one of these was found by actually building and running the thing, not by inspection.
database finalize when it
finds zero matching files — it does not just report zero findings the way
build-mode: none does for a language that does have source present. Fixed by
dropping javascript-typescript until real JS/TS code exists (Phase 3+) and adding
csharp instead, once real C# source (ServiceDefaults) landed.
csharp fix above then failed its own build: Aspire 13's AppHost SDK requires
the Aspire CLI bundle to compile at all (ASPIRE009), and hosted GitHub runners
don't have it. See the Judgment Calls section below for how this one was actually fixed rather
than just patched over.
/language:javascript-typescript
analyses from the original broken runs were still sitting there, permanently "errored," with
nothing superseding them. Fixed by deleting all 7 via the code-scanning API directly.
No such host is known (usage-aggregation:80).
AppHost.cs registered meter-simulator without
.WithReference(usageAggregation), so Aspire never injected the service-discovery
config the DNS name needed to resolve. Caught only because the run was actually verified against
real output instead of trusting a clean build — fixed, then re-verified live: 1,876/1,876
readings delivered.
dotnet test is outright broken for
xUnit v3 on the .NET 10 SDK ("VSTest target is no longer supported") unless a project opts into
the new Microsoft.Testing.Platform runner mode — a repo-root global.json
setting most guides don't mention yet, since it's brand new with .NET 10.
Microsoft.EntityFrameworkCore.Relational version conflict —
the app's own EF Core Design package forced 10.0.12 while the test project's transitive Npgsql
dependency settled on 10.0.11. Fixed by pinning the version explicitly rather than ignoring the
warning.
.csproj used Microsoft.NET.Sdk.Worker alone, which
doesn't pull in Microsoft.Extensions.Hosting as a framework reference —
Host.CreateApplicationBuilder/BackgroundService/ILogger<T>
all failed to resolve, 7 compile errors from one missing package reference.
OptionsValidationException
internally during its own StartAsync(), before any caller's try/catch
ever sees it. Fixed by resolving the options manually before host.Run(), so
validation happens in application code, never reaching the Host's internal failure logging.
Expected: 10, Actual: 50. The test used a hardcoded meter ID against
AppHost's Postgres, which uses a persistent data volume by design for real local dev —
so every run kept adding its reading to the same row left over from the last one. Fixed by generating
a unique meter ID per run; re-verified by running it twice in a row against the same persistent
database.
Expected 1 brokers but found only 0). Its bootstrap-servers env var was wired to
Kafka's external, host-facing endpoint (Resource.Host/.Port),
which isn't reachable from inside another container's own network namespace. Fixed with
kafka.Resource.InternalEndpoint.Property(EndpointProperty.HostAndPort) — the
container-to-container address, found by reflecting on the real installed
Aspire.Hosting.Kafka assembly rather than guessing. Re-verified live: schema
registry connects and GET /subjects returns 200 OK.
HttpIOException: The response ended prematurely
calling the schema registry. WaitFor(schemaRegistry) without a configured health
check only waits for the container to reach Running, not for the app
inside it to actually be ready to serve requests. Fixed by adding
.WithHttpHealthCheck("/subjects") to the resource. Re-verified live: 924/924
readings sent successfully, 0 failures.
ReadingProcessor's constructor to require a Kafka producer broke
UsageAggregation.Tests on main (error CS7036) — caught
only because CI builds the full solution on every push, while local verification during
this step had only built the single changed project. Fixed properly, not just patched: introduced
a small project-owned IUsageAggregatedPublisher interface instead of depending
directly on Confluent.Kafka's much larger IProducer<TKey,TValue> — a genuine
SOLID improvement that also let the test use a trivial hand-rolled fake, no mocking library
needed.
Running to Finished about 12 seconds in, with no
restart. Root cause: Program.cs calls dbContext.Database.Migrate() on
startup, and the migration's new unique index failed against real data — 5 pre-existing
Invoices rows shared the same key, left over from repeated Phase 1 smoke-test runs
that predated the upsert logic. Fixed by adding a dedup step to the migration itself (keeping the
most-recently-generated row per key) before creating the index, so it's safe against any
environment with pre-existing duplicate data, not just this one. Re-verified live: 91 real
Invoice rows landed with correct math and the upsert confirmed working in place.
Could not create HTTP
health check for resource 'account-customer' as no endpoint was found matching one of the
specified names: https, http. The hand-scaffolded project had no
Properties/launchSettings.json — every other service in this repo has one because
it was generated by dotnet new tooling, this one was hand-written, and Aspire's
AddProject<T>() reads that file to discover HTTP endpoints. Fixed by adding a
proper launchSettings.json and pinning the health check to the http
endpoint explicitly.
accountId field to meter.readings.raw's schema silently
broke every publish — the topic stayed pinned at schema version 1 in the registry, meaning the
new field was never actually accepted. Confluent Schema Registry defaults to
BACKWARD compatibility, which requires a new field to carry a default
value; it had none. Fixed by adding "default": "". Re-verified live: schema
version 2 registered cleanly, real accountId values confirmed for every meter.
yarn install's peer-dependency warning about vite looked like routine
noise. It wasn't: vitest 5.0.0 requires vite as a real runtime
dependency, and without it yarn test failed outright with
ERR_MODULE_NOT_FOUND before running a single test. Fixed by adding vite
to the shared Yarn Berry catalog and this service's dev dependencies.
@kafkajs/confluent-schema-registry
decodes strictly against the writer's registered schema, with no reader-schema projection — an
old message from before the AccountId re-keying decoded to
accountId: undefined, which threw, which kafkajs retried forever on the same
offset until the consumer crashed out of its group entirely. Fixed by skipping messages with a
missing accountId instead of letting the exception propagate. Re-verified live:
skipped ~4,387 old incompatible messages, then delivered real webhooks for genuinely new
invoices.
actions analysis flagged both new test workflows as missing
an explicit permissions block, falling back to the default (broader)
GITHUB_TOKEN scope instead of least privilege. Fixed with a top-level
contents: read on each — all either ever does is checkout code and run tests.
eachMessage that kafkajs doesn't catch on its own. Fixed by wrapping the
account lookup and webhook send in a try/catch, logging and
moving on rather than taking the whole consumer down over one account being unreachable.
UsagePoint collection endpoint surfaced a phantom
account entry with an empty id and self-link. Traced to 19,888 real
ProcessedReadings rows with AccountId = '', left behind by a
long-forgotten orphaned MeterSimulator process running a
pre-account-enrichment build — confirmed, not assumed, since the current code physically
cannot produce an empty AccountId. Fixed three ways: the corrupted rows were
deleted (a fresh backup taken first), a guard was added to skip and log any future message
with no AccountId, and the ESPI collection query itself filters blank ids as
defense-in-depth.
StripeException — and
because .NET's default BackgroundServiceExceptionBehavior is
StopHost, that one exception crashed Billing's entire process, taking the
pre-existing, Stripe-unrelated core invoice-generation consumer down with it. Caught by
the existing full-stack smoke test unexpectedly failing. Fixed with an explicit guard
skipping Stripe collection for invoices under $0.50, plus a try/catch around the whole
call so no future Stripe API failure can crash the host again.
ProcessedReadings from a previous dev
session — already older than the stale threshold — got flagged as brand-new anomalies on
the detector's very first sweep, including the already-known
SMOKE-TEST-METER-* rows from issue #28, confirming that pollution now reaches
Grid Operations too, not just accountsdb. Fixed by adding a maximum lookback
window — a reading only signals a new anomaly if it's recent enough to mean the
meter just went quiet, not simply old, pre-existing data.
idsrv cookie defaults to SameSite=None, which
also requires Secure; running locally over plain HTTP, the browser silently
dropped it rather than rejecting the response outright. Fixed by forcing
SameSiteMode.Lax for the idsrv scheme. Re-verified live: login now
persists across requests.
/api/* gateway endpoint rejected a real, freshly-issued access token with
401, despite the JWT decoding to valid claims. Decoding the real token showed why:
its aud claim was Duende's static-audience fallback, not grid-ops-api
— no ApiResource was registered for that scope. The first attempted fix had zero
effect, caught only by re-testing live: the registration call itself was never actually wired
into HostingExtensions.cs. Fixed by adding it. Re-verified live: real work-order
and outage data now flows through the gateway with a real token.
No bootstrap servers
configured. aspire logs showed the real exception directly: its AppHost
registration was missing the Kafka/schema-registry references the anomaly-relay consumer
needs, deferred during earlier scaffolding and never added back once that consumer was wired
up. Fixed by adding both references. Re-verified live: the gateway starts cleanly and relays a
real anomaly from a real stopped meter.
Early, and said so — not oversold
Six services — Account/Customer Service, Meter Simulator, Usage Aggregation, Billing, Grid
Operations, and a polyglot Node.js Notification Service — all built, wired together through
AppHost, and verified against a real running stack: real Kafka events through a real
Confluent Schema Registry, real AccountId-keyed invoices with correct math, and a real
webhook actually received. Usage Aggregation also exports real Green Button/NAESB ESPI usage data
over HTTP, Billing collects real Stripe test-mode payments — a real card charged, a real webhook
verified and applied — and Grid Operations correlates real outages and work orders from meters
that stop transmitting, confirmed against a real stopped meter. Backed by 102 automated tests,
including an end-to-end smoke test that spins up the whole stack and walks a reading all the way
through to a delivered invoice. Phase 4a — the Grid Operations console's Identity server, BFF
gateway, and React frontend — is all built and live-verified too; automated tests and UI polish
still outstanding.
Every number on this page is reproducible from this repository's own commit history and issue tracker. See todo.md for current phase-by-phase status rather than assuming it's further along.
Where the interesting decisions actually happened
The AppHost build failure above was first worked around with a solution filter excluding
AppHost from the CI build entirely — a legitimate pattern, already used for the same
Aspire-13 issue on eshop-full. But
AppHost.cs was about to gain real orchestration logic (the Postgres resource wiring)
worth actually building and analyzing, not quietly excluding forever. That filter was reverted in
favor of installing the real Aspire CLI in CI instead, pinned to the exact version the NuGet packages
use — a few extra seconds per run, in exchange for CI building and CodeQL-analyzing the actual,
complete solution instead of a narrower stand-in for it.