Skip to main content
Experiment · Architecture

Connect Analyzer

A sales dashboard in .NET 10 and Next.js built as a craftsmanship experiment: the data source lives behind a port, so going from a mock to real SAP S/4HANA or Shopify is just writing a new adapter. The rest of the system never notices.

The problem

I need to render a dashboard with SAP data. Today there is no SAP: there is a .txt mimicking its export. Tomorrow an OData sandbox; the day after, a real store. How do I structure the code so that this dance does not force me to rewrite the backend every time the source changes?

The solution

  • One port for the source

    The ISalesRepository interface is the only contract that knows where data comes from. Everything else depends on the contract, never on the implementation.

  • Three adapters, one interface

    Mock (.txt in Latin-1), SAP S/4HANA over OData, and Shopify over the Admin REST API. One env var (SalesSource) picks which one is wired at the composition root.

  • Persistence behind a second port

    Ingestion reads from the source and saves into SQLite (ISalesStore); analytics read only from the store. Moving to Postgres would be another adapter, without touching the domain.

  • Actually deployed

    Backend and mock on Google Cloud Run, frontend on Vercel, with automatic deploys via GitHub Actions and Workload Identity Federation (no keys stored as secrets).

Tech stack

Backend

  • .NET 10
  • C#
  • Hexagonal
  • Result/Error
  • SQLite
  • xUnit

Frontend

  • Next.js
  • TypeScript
  • App Router
  • Recharts

Infra & Deploy

  • Docker
  • Cloud Run
  • Vercel
  • GitHub Actions
  • OData (SAP)
  • Shopify

Technical decisions

  • Data source behind a port

    An inviolable rule: ISalesRepository is the only thing that knows the source. Adding a new one = an outbound adapter + swapping its registration in Program.cs. The domain never finds out.

  • Result/Error instead of exceptions

    Expected errors are values (Result), not exceptions. The adapter catches them at its edge; the controller is the only place that translates Error → HTTP. I cover it in detail in the post.

  • CI/CD without keys

    The Cloud Run deploy authenticates with Workload Identity Federation: GitHub Actions gets ephemeral Google Cloud credentials without storing any service-account key as a repo secret.

Screenshots

Connect Analyzer dashboard header with filters and KPI cards: total revenue, sales, average ticket, units, customers and products

Connect Analyzer dashboard header with filters and KPI cards: total revenue, sales, average ticket, units, customers and products

Revenue-over-time area chart of the dashboard, with a tooltip showing the value for a specific day

Revenue-over-time area chart of the dashboard, with a tooltip showing the value for a specific day

Dashboard charts: revenue bars with a units line by product, and a total-amount-by-customer donut

Dashboard charts: revenue bars with a units line by product, and a total-amount-by-customer donut

How is it built inside?

I wrote a long post about the architecture: ports, adapters, Result/Error and testing without mocking libraries.