Logo
domain modelingproduct engineeringsubscriptionsarchitecturecarizmo

Your subscription is a domain model, not a payment record

I designed Carizmo's subscriptions as monthly credits. Customers thought in calendars. Here's how the clean model collided with reality — and the separation that finally made it work.

6 min read
Your subscription is a domain model, not a payment record

TL;DR — I designed Carizmo's subscriptions as monthly credits. Customers thought in calendars. Payments and services aren't the same lifecycle. When the domain model reflects the promise you actually made, complexity disappears — when it doesn't, the leak shows up everywhere.

When I first designed subscriptions for Carizmo, the model looked straightforward.

A customer subscribes to a plan. The plan gives them a fixed number of car washes every month. They choose their preferred days. We generate the wash schedule. Done.

At least, that was the theory.

Then real customers started using it.

And the clean subscription model I had designed started colliding with calendars, missed appointments, payment behavior, operational capacity, and the simple fact that months do not have exactly four weeks.

That was when I learned an important lesson:

A subscription is not just a payment record. It is a domain model.

And if the model does not reflect how the business actually operates, the complexity eventually leaks everywhere else.

The original model

Carizmo offers recurring mobile car-wash services — anywhere from one to five washes per week. The first implementation represented these as monthly allowances: 8, 12, or 20 washes per month.

From a software perspective, this was convenient. A subscription had a number of washes available. Each completed wash reduced the remaining balance. When the balance reached zero, the customer had consumed the month's entitlement.

Simple.

Except the customers did not think in monthly credits. They thought:

Wash my car every Sunday and Wednesday.

That distinction became important very quickly.

Calendars don't care about your pricing model

ChatGPT Image Aug 27, 2026, 05_50_37 AM

Imagine a customer has a twice-per-week plan. Two washes per week sounds like eight washes per month.

Except some months contain four Mondays. Some contain five.

A customer who chooses Monday and Thursday may sometimes naturally have nine wash days in the same calendar month.

Now the system has a problem. Do you:

  • skip one of their expected days?

  • charge them for an extra wash?

  • move the wash into the next billing cycle?

  • increase the monthly allowance?

  • define the plan by weeks rather than months?

None of those answers is purely technical. They change the product.

The original database structure was not necessarily wrong. The assumption behind it was. I had modeled what was easy to count rather than what the customer believed they were buying.

Then missed washes made it worse

Field-service products rarely operate entirely on the happy path.

Sometimes the customer is unavailable. Sometimes the car is covered. Sometimes access to the parking area is blocked. Sometimes the team cannot complete the service because of conditions at the location.

A scheduled wash can therefore end in several different ways:

Scheduled
    ↓
Assigned
    ↓
Attempted
   ↙  ↓  ↘
Completed  Customer unavailable  Operational failure

And now another question appears: does an unsuccessful attempt consume one of the customer's washes?

Again, there is no universal answer.

If the cleaner arrives at 2:00 AM, finds the vehicle unavailable, and has already spent the operational capacity required to service it, the business may consider that appointment consumed. The customer may see it differently.

The software needs to represent that distinction explicitly. Trying to express all of this with something like:

remainingWashes -= 1

quickly becomes dangerous.

Payment status was another hidden trap

Another assumption was that subscription renewal and payment should happen together.

It sounds reasonable:

payment succeeds
      ↓
subscription renews

But real businesses are more complicated. Some customers pay immediately. Others have agreed payment terms. Some companies settle invoices at the end of the month. Some customers temporarily have outstanding balances but should still remain operationally active.

That means these concepts are related, but not identical:

Subscription lifecycle
Payment lifecycle
Service entitlement
Scheduling

If all four are represented by one status field, the model eventually becomes impossible to reason about.

For example:

subscription.status = "ACTIVE"

What exactly does that mean?

  • The subscription period is active?

  • The customer has paid?

  • The customer can receive services?

  • The schedule has been generated?

  • Automatic renewal is enabled?

Those are different questions.

Separate the concepts

ChatGPT Image Aug 27, 2026, 05_50_45 AM

The more robust model was to treat them independently.

Customer
   │
   └── Subscription
          │
          ├── Billing Period
          │      └── Payment
          │
          ├── Service Entitlement
          │
          └── Wash Jobs

A subscription describes the commercial relationship. A billing period describes a specific period. A payment records the financial state. An entitlement describes how much service the customer is allowed to receive. A wash job describes what actually happened operationally.

That separation makes questions much easier to answer.

  • Has the customer paid? Look at payments.

  • Should the customer's subscription renew? Look at the subscription lifecycle.

  • Can another wash be scheduled? Look at entitlement.

  • What happened last Wednesday? Look at the wash job.

That sounds obvious after the system has been redesigned. It was much less obvious when everything started as a simple monthly counter.

State machines became more useful than booleans

Another lesson was to avoid reducing real workflows to boolean flags. A field-service job is rarely just:

completed: true

A more useful lifecycle looks something like:

SCHEDULED
    ↓
ASSIGNED
    ↓
IN_PROGRESS
    ↓
COMPLETED

But alternative outcomes matter just as much:

SCHEDULED
    ↓
ATTEMPTED
   ↙       ↘
NOT_FOUND   BLOCKED
     ↓
RESCHEDULED

Each transition can have business consequences. It may consume entitlement, trigger a notification, change cleaner compensation, require rescheduling, affect reporting, or affect billing. Once those consequences exist, the states deserve to exist too.

The database was not the hardest part

The interesting part of this problem was not PostgreSQL, Prisma, React Native, or Next.js. Those were implementation tools.

The difficult part was understanding the business well enough to represent it accurately. That meant spending time looking at questions such as:

  • What does the customer believe they purchased?

  • What does the operations team consider a completed attempt?

  • What happens when service cannot be delivered?

  • Which events affect revenue?

  • Which events affect scheduling?

  • Which events should customer support be able to override?

A technically elegant schema based on the wrong assumptions is still a bad schema.

Real operations are excellent architecture tests

Building software for a business I was operating myself changed how I think about domain modeling.

When you are several layers removed from operations, requirements can look clean. You receive something like:

Customers have eight washes per month.

So you build eight washes per month.

But when you operate the business, you see what that sentence hides. You see the customer who always expects Wednesday. You see the fifth Wednesday. You see the cleaner who arrived but could not access the vehicle. You see the invoice that is intentionally paid later. You see the support team trying to manually correct an entitlement.

Those edge cases are not really edge cases. They are the domain.

The rule I use now

When designing subscription systems, I no longer begin with:

How many credits should this plan have?

I start with:

What promise are we making to the customer?

Then I model everything required to represent that promise. The implementation comes afterward.

Because when the domain model is wrong, every feature built on top of it becomes harder. And when the domain model reflects reality, a surprising amount of complexity disappears.

More Articles

Continue reading with these related posts