The hard part of field-service software is modeling failure
Every field-service app has a beautiful three-state workflow: Booked → Assigned → Completed. Then customers aren't home, gates don't open, and cars aren't there. Modeling that reality is where the actual engineering lives.

TL;DR — Booking demos always model success. Production is 30% success, 70% "the car isn't there, the gate is locked, the cleaner got delayed." State machines beat booleans, failed appointments need first-class treatment, and the admin interface is where the real business lives.
Most service-booking applications look simple in a product demo.
A customer books something. A worker gets assigned. The worker performs the job. The customer receives a notification.
Booked → Assigned → CompletedThen you deploy it into the real world.
The customer is not there. The address is wrong. The vehicle is inaccessible. The technician's previous job took longer than expected. A gate does not open. The service cannot be performed because of weather, security, equipment, or location constraints.
Suddenly your beautiful three-state workflow is not a workflow. It is a wish.
One of the biggest things I learned while building Carizmo is this:
The difficult part of field-service software is not modeling success. It is modeling everything that prevents success.
The happy path is easy
Consider the ideal flow for a mobile car-wash service.
Customer
↓
Subscription
↓
Scheduled Wash
↓
Cleaner Assigned
↓
Cleaner Arrives
↓
Car Washed
↓
CompletedNearly every engineering team can implement this. You need a customer, an address, a vehicle, an appointment, an assigned worker, and a status. The interfaces are straightforward. The database tables are straightforward. The API is straightforward.
Most prototypes stop here. Production cannot.
What happens when the car isn't available?

This was one of the first operational problems that exposed the weakness of a simplistic workflow.
Imagine a customer has a scheduled wash at night. The cleaner reaches the location. The car is not there.
What should the application record?
A naive implementation might mark the wash as cancelled. But cancelled by whom? That matters.
There is a large difference between:
CUSTOMER_CANCELLEDand:
CUSTOMER_NOT_FOUNDAnd both are different from:
OPERATION_CANCELLEDThose distinctions affect several systems simultaneously.
Customer entitlement
Should the wash still count?
Cleaner compensation
Did the cleaner perform enough work to receive credit for the visit?
Scheduling
Should the system automatically create another appointment?
Reporting
Was the failure caused by the customer or by operations?
Customer support
Can someone override the outcome?
The moment those questions matter, "CANCELLED" stops being a useful status.
Failure needs structure
A better model treats unsuccessful outcomes as first-class states.
SCHEDULED
│
▼
ASSIGNED
│
▼
ARRIVED
│
├──────────────► COMPLETED
│
├──────────────► CUSTOMER_NOT_FOUND
│
├──────────────► VEHICLE_INACCESSIBLE
│
├──────────────► LOCATION_BLOCKED
│
└──────────────► OPERATION_ABORTEDSome of those states may allow rescheduling. Others may be terminal. More importantly, they carry different business meaning.
This is where a state machine becomes much more useful than a collection of booleans.
Boolean fields hide impossible combinations
It is tempting to design something like:
{
completed: false,
cancelled: true,
rescheduled: true,
customerAbsent: true
}But now the application has to understand combinations. Can something be both completed and cancelled? Can a completed wash be rescheduled? Can a customer be absent after the cleaner completed the service?
The database may technically allow all of those combinations. The business cannot.
A state machine makes valid transitions explicit:
SCHEDULED -> ASSIGNED
ASSIGNED -> ARRIVED
ARRIVED -> COMPLETED
ARRIVED -> CUSTOMER_NOT_FOUND
CUSTOMER_NOT_FOUND -> RESCHEDULEDAnd perhaps COMPLETED -> RESCHEDULED is simply forbidden. That removes an entire class of application bugs.

A job status is not enough either
As the system became more complex, I also learned that operational state should not automatically control every other part of the product.
For example, consider CUSTOMER_NOT_FOUND. That state alone does not tell you whether the customer should lose a wash credit. That is a business policy.
const consumeEntitlement =
job.status === "COMPLETED" ||
job.failurePolicy === "CHARGEABLE_ATTEMPT";The distinction matters because policies change. Maybe the first missed appointment is free. Maybe repeated missed appointments consume entitlement. Maybe enterprise customers have different rules.
If the operational state and commercial rule are hard-coded as the same concept, every policy change becomes a migration.
Physical businesses create cascading effects
Software products sometimes treat an unsuccessful action as cheap. If a user abandons an online checkout, nothing physical happened.
Field operations are different. A failed appointment may already have consumed cleaner time, transportation, fuel, equipment availability, route capacity, and management coordination.
That means a single failure influences:
Customer experience
│
├── Scheduling
├── Billing
├── Staffing
├── Route capacity
├── Cleaner incentives
└── ReportingThat is why these systems become difficult. The complexity is not primarily technical. It comes from trying to accurately represent the physical world.
Rescheduling is a business process, not a date update
Another trap is treating rescheduling as:
job.date = newDate;That destroys useful history. If a customer was unavailable on Monday and the service was moved to Wednesday, those are two different operational facts.
The system should be able to answer:
How many appointments were missed this month?
How many required rescheduling?
Which areas generate the most failed attempts?
How much operational capacity are missed appointments consuming?
If you overwrite Monday with Wednesday, that information disappears.
A better structure is closer to:
Wash Job #120
Monday
CUSTOMER_NOT_FOUND
↓ rescheduled toWash Job #134
Wednesday
SCHEDULED
Now both events remain visible. That is useful operationally and analytically.
Auditability matters earlier than you expect
As soon as humans operate a system, someone eventually asks: Who changed this? Why did this customer lose a wash? Why was this appointment moved?
A field-service system benefits heavily from recording transitions:
JobHistoryjob_id
from_status
to_status
changed_by
reason
timestamp
That information is cheap to capture when the architecture is designed for it. It is painful to reconstruct months later.
Admin interfaces become operational software
Another thing I underestimated initially was the importance of the admin side. Customer-facing applications get most of the design attention. But in a service business, the internal interface may have more impact on the quality of the service than the customer application.
Operations need to understand: what is scheduled, what is late, what failed, why it failed, which cleaner owns it, whether the customer should be contacted, whether it should be rescheduled, whether entitlement was consumed.
If those answers require opening five pages, people will create their own workflow outside the software. Usually in WhatsApp or spreadsheets. Once that happens, the application is no longer the source of truth.
The admin experience therefore needs to optimize for decisions, not database entities.
Real-world software needs escape hatches
Another lesson was not to assume every scenario can be predicted. You need rules. But you also need controlled overrides.
For example, support may need to restore a wash credit after an unusual situation. That should not require manually editing a database.
The system can support actions such as: restore entitlement, reschedule without charge, override failure reason, reassign cleaner, mark operational exception.
But every override should leave a trace.
Good operational software combines: automation + rules + human override + audit history. Removing any one of those usually creates problems.
Design for failure before adding features
I now look at field-service systems differently. When someone proposes a workflow like:
Book → Assign → CompleteI am less interested in the arrows. I want to know what happens when each arrow fails.
What happens if assignment fails?
What happens if the worker cannot arrive?
What happens if the customer is unavailable?
What happens if the job starts but cannot be completed?
What happens if the result is disputed?
Those answers tell you more about the architecture than the happy path does.
The broader lesson
This lesson applies far beyond car washing. It appears in delivery, logistics, home maintenance, healthcare appointments, construction, repair services, transportation, and property management.
Anywhere software coordinates humans in the physical world, failure is not exceptional. It is part of normal operation.
The strongest systems do not pretend otherwise. They model it.
More Articles
Continue reading with these related posts



