Portfolio Manifest — Updated September 20, 2026

Terrence Daniels

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.

Phase 4aIdentity + gateway + frontend live
296commits
30issues tracked
26real bugs found
13.5.4Aspire CLI
102tests, all passing

Why this project

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.

.NET 10 .NET Aspire 13 Kafka React 19 Redux Toolkit / RTK Query TimescaleDB / Postgres Node.js GitHub Actions OpenTelemetry

Built one phase at a time, in the open

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.

Why event-driven, not a monolith

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.

Tracked before it was built

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.

26 real bugs, found by actually running the thing

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.

01
.github/workflows/codeql.yml
CodeQL's JavaScript/TypeScript extractor hard-fails 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.
real CI failure · tooling issue #3
02
src/AppHost/GridPulse.AppHost.csproj
The 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.
real CI failure · tooling issue #3
03
GitHub Security overview page
After both fixes above landed, the Security tab kept showing a "Code scanning configuration error" banner anyway. Root cause: GitHub's tool-status tracker doesn't clear a language just because a later workflow stops analyzing it — 7 failed /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.
real finding · stale state, not live code issue #3
04
src/AppHost/AppHost.cs
The first live run of Meter Simulator → Usage Aggregation sent 0 of 7 readings successfully — every attempt failed with 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.
real bug · live E2E only issue #6
05
global.json
Adding the first test project revealed that 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.
real tooling break · .NET 10 SDK issue #16
06
tests/UsageAggregation.Tests/*.csproj
Adding EF Core's InMemory provider for fast unit tests alongside the real Npgsql provider produced an unresolved 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.
real dependency conflict issue #17
07
src/MeterSimulator/GridPulse.MeterSimulator.csproj
The hand-written .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.
real build failure issue #6
08
src/MeterSimulator/Program.cs
Fail-fast config validation worked — the app exited non-zero with a clean message — but a full stack trace printed first anyway. .NET's Generic Host logs 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.
real bug · noisy failure path issue #6
09
tests/AppHost.Tests/Phase1SmokeTests.cs
Right after declaring Phase 1 done, re-running the smoke test a second time failed: 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.
real bug · test isolation issue #10
10
src/AppHost/AppHost.cs
The schema registry's first live run failed to connect to Kafka at all (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.
real bug · container networking issue #11
11
src/AppHost/AppHost.cs
The first live run after wiring Meter Simulator's Kafka producer sent 274 of 280 readings successfully — 6 failed with 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.
real bug · startup race issue #11
12
src/UsageAggregation/ReadingProcessor.cs
Changing 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.
real bug · CI break issue #11
13
src/Billing/Migrations/20260913120150_AddInvoiceDueDateAndUniqueIndex.cs
The first live run exercising Billing's new Kafka wiring crashed it at startup — its resource state went from 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.
real bug · migration vs. real data issue #19
14
src/AccountCustomer/GridPulse.AccountCustomer.csproj
The first live run of the new Account/Customer Service threw 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.
real bug · live E2E only issue #20
15
src/UsageAggregation/Avro/MeterReadingRaw.cs
Adding an 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.
real bug · schema evolution issue #21
16
src/notification-service/package.json
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.
real bug · missing runtime dependency issue #23
17
src/notification-service/src/notificationConsumer.ts
Live verification found the Notification Service's Kafka consumer group sitting at zero members after several minutes with no webhooks delivered. @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.
real bug · poison message issue #24
18
.github/workflows/test.yml
This repo's own CodeQL 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.
real finding · CodeQL self-check, not live
19
src/notification-service/src/notificationConsumer.ts
Live verification found the Notification Service's consumer crashing outright on any account-lookup or webhook-delivery failure — an unhandled rejection inside 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.
real bug · unhandled rejection issue #25
20
src/UsageAggregation/MeterReadingConsumer.cs
Building the ESPI export's 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.
real bug · legacy Kafka data issue #26
21
src/Billing/InvoicePaymentConsumer.cs
Building the Stripe integration's payment-initiation consumer, a real invoice below Stripe's actual $0.50 USD Checkout Session minimum (a real hourly-usage fraction of a kWh, not a contrived edge case) threw an unhandled 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.
real bug · unhandled exception, host crash issue #27
22
src/GridOperations/OutageCorrelator.cs
Live-verified by stopping the meter simulator with 20 real meters active and letting them all go quiet together: the result was 5 separate 2-meter outages plus several orphaned standalone drafts sitting inside those outages' street-number ranges, instead of the one shared work order the design called for. Root cause: the correlator only ever paired a new anomaly with the single nearest existing draft, never re-checking whether other drafts now fell inside a growing outage's range. Fixed by redesigning the correlator to extend an existing outage's range (absorbing newly-in-range orphaned drafts) or merge a whole cluster of nearby drafts into one new outage, either way keeping exactly one surviving work order.
real bug · correlation logic issue #29
23
src/UsageAggregation/UsageAnomalyProcessor.cs
On a fresh AppHost start, real leftover 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.
real bug · cold-start false positive issue #30
24
src/Identity/HostingExtensions.cs
Logging in via Duende's Quickstart UI succeeded server-side, but the very next request came back unauthenticated — no error, no redirect loop, just silently logged out. Direct server log evidence showed why: the 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.
real bug · silently dropped auth cookie issue #31
25
src/Identity/Config.cs
Every /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.
real bug · OIDC audience misconfiguration issue #32
26
src/AppHost/AppHost.cs
The Grid Operations BFF gateway crashed on every AppHost run with 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.
real bug · missing AppHost wiring issue #33

Where this actually stands right now

Early, and said so — not oversold

Phases 1 through 3 are done, plus ESPI, Stripe, and Grid Operations, all verified live

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.

Nothing here is unverified

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.

Judgment calls

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.