What I wanted to learn by building Subscription Manager

Why I built a small .NET application that was simple enough to finish, but real enough to learn from.

I have worked with .NET for many years, but most professional software is built inside an existing context.

The architecture already exists. Infrastructure is already there. Many decisions have already been made, often long before you join the project.

I wanted a small project where I could make those decisions myself and then see what happened when they had to work together.

That became Subscription Manager.

Something small enough to finish

Subscription Manager started as a fairly simple application for managing recurring subscriptions and tracking their costs.

The first version did not need a complicated domain. It needed subscriptions, an API, persistence, a web application and some tests.

Subscription Manager
│
├── subscriptions
├── API
├── Blazor
├── persistence
└── tests

The limited scope was intentional.

I was not trying to reproduce every problem a commercial SaaS product might have. I wanted something small enough to understand and finish, while still leaving room for more realistic problems to appear.

Subscriptions worked well for that.

The basic use case was simple, but it naturally led to users, accounts, currencies, external services and eventually payments.

Making it more real

Authentication was one of the first things that changed the character of the application.

Adding login and registration was not particularly interesting by itself. The useful problems appeared around them.

Sessions could expire. Email addresses needed confirmation. Accounts needed a lifecycle. Deleting an account also meant deciding what should happen to the user's data.

Those details made the application more useful as a learning project because features were no longer isolated.

A change in one place could affect another part of the application.

That was what I wanted to experience.

External systems changed the project

The application eventually integrated with several external services.

They looked similar at first because they were all APIs, but they introduced quite different problems.

Integration What it introduced
NBP External reference data and currency conversion
OpenAI Non-deterministic output
Stripe External state, payments and webhooks

The NBP integration was relatively predictable. The application requested exchange rates and used them to compare subscription costs in different currencies.

OpenAI was different.

I added a savings-plan feature that used subscription data to suggest possible ways to reduce recurring costs.

Getting the model to return something useful was the easy part. Making the feature behave like part of an application took more work.

It needed configuration, error handling, usage limits and tests around the parts of the behaviour that the application could control.

Stripe introduced another kind of boundary. A payment provider could now change the state of a subscription outside the application, which meant handling checkout, subscription changes and webhooks.

Using three different services in one small project was more useful to me than building three separate API demos.

Running somewhere other than my computer

For a while, most of the application lived comfortably on my development machine.

Moving it towards production exposed another set of problems.

Configuration had to be correct for a specific environment. The database had to exist and be migrated. Secrets needed somewhere to live. GitHub Actions had to authenticate to Azure. The application needed a way to report whether it was healthy.

code
 ↓
CI
 ↓
infrastructure
 ↓
configuration
 ↓
deployment
 ↓
running application

Some things needed correction along the way.

An OIDC subject did not match. A deployment database check needed fixing. Configuration could fall back in ways I did not want. Production settings behaved differently from local development.

None of those problems was especially dramatic.

Together, though, they were a useful reminder that running an application locally hides an entire category of work.

I did not need to build everything

The goal was never to make Subscription Manager an example of every technology I knew.

It did not need every pattern, every infrastructure component or every feature that a larger product might eventually require.

Adding more things would have made the repository bigger, but not necessarily more useful.

I wanted enough moving parts to make their interactions real while keeping the project small enough that I could still understand and change the whole thing.

That constraint became part of the experiment.

When a new piece had a concrete purpose, I could add it and see what changed. When it did not, there was no reason to add it just to make the architecture look more complete.

What I took from it

By the time Subscription Manager was running in production, it had become much more than the CRUD application I started with.

It had users, persistence, external integrations, AI-assisted functionality, payments, tests, infrastructure and a deployment process.

More importantly, I had seen those pieces grow together rather than only looking at the finished structure.

Once the application was working, another question appeared.

If I started a second product, which parts of this project would I actually want again?

Copying the repository would have been easy. Deciding what deserved to be reused was more interesting.

That question eventually led me to Launchpad.