Exception Workflows Overview
Not every parcel reaches its destination on the first attempt. Delivery exceptions are a normal part of logistics operations. A robust parcel tracking API must model these failure scenarios, support retry workflows, and provide operational visibility into parcels that need attention.
What Is a Delivery Exception?
A delivery exception occurs when a carrier cannot complete a delivery as planned. The parcel transitions from its current status to Exception, and the system records the reason for the failure. Common exception reasons in our domain include:
| Reason | Description |
|---|---|
AddressNotFound | The delivery address does not exist or cannot be located |
RecipientUnavailable | No one was available to accept the package |
DamagedPackage | The package was damaged during transit |
WeatherDelay | Severe weather prevented delivery |
CustomsHold | The parcel is held at customs for inspection |
RefusedByRecipient | The recipient declined to accept the package |
Each of these reasons represents a different operational scenario. Some are temporary and eligible for retry (like RecipientUnavailable), while others may require manual intervention (like DamagedPackage).
The Exception-Retry Lifecycle
When a delivery attempt fails, the parcel follows a specific lifecycle:
1InTransit / OutForDelivery2 │3 ▼ (delivery fails)4 Exception5 │6 ├──► Retry requested ──► InTransit (attempt count incremented)7 │ │8 │ ▼9 │ Next delivery attempt10 │ │11 │ ├──► Success ──► Delivered12 │ │13 │ └──► Fails again ──► Exception14 │15 └──► 3 failed attempts reached ──► Returned
The cycle between Exception and InTransit can repeat up to a maximum number of delivery attempts. After 3 failed attempts, the parcel automatically transitions to Returned status, indicating it will be sent back to the sender.
Attempt Tracking
Recall from Topic 1 that the Parcel entity includes a DeliveryAttempts field. Until now this field has remained at its default value of zero --- no previous topic needed to modify it. In this topic, we use it to track how many times delivery has been tried. This counter is critical for enforcing the maximum retry limit.
csharp1public class Parcel2{3 // ... other properties45 public int DeliveryAttempts { get; set; }6 public ParcelStatus Status { get; set; }7}
Each time a delivery exception is reported, the DeliveryAttempts counter increments by one. When a retry is requested, the system checks this counter against the maximum allowed attempts before permitting the transition back to InTransit.
The ExceptionReason Enum
Exception reasons are modeled as an enum to ensure consistency and enable filtering:
csharp1public enum ExceptionReason2{3 AddressNotFound,4 RecipientUnavailable,5 DamagedPackage,6 WeatherDelay,7 CustomsHold,8 RefusedByRecipient9}
This enum appears in two places: the API request body when reporting an exception, and the tracking event record that logs the exception.
Tracking Events for Exceptions
Every exception and retry generates a tracking event. This preserves the full audit trail:
- Exception reported: A
DeliveryAttemptedevent is created with the exception reason in theDelayReasonfield and a descriptive message - Retry scheduled: A new tracking event is created with a description like "Redelivery scheduled" and the new estimated delivery date
- Auto-return: When the maximum attempts are reached, a
Returnedevent is created explaining the return reason
These events allow anyone viewing the tracking history to see exactly what happened and when. Here is an example of what the tracking timeline looks like for a parcel that was retried once and then delivered:
12025-02-15 08:00 InTransit "Parcel departed facility in Chicago, IL"22025-02-15 14:30 DeliveryAttempted "Delivery exception: RecipientUnavailable"32025-02-16 09:00 InTransit "Redelivery scheduled. New estimated delivery: 2025-02-17"42025-02-17 11:15 OutForDelivery "Out for delivery in Springfield, IL"52025-02-17 14:45 Delivered "Delivered to front door"
The tracking history tells a complete story. A customer or operations agent reviewing this timeline can see exactly when each attempt occurred and what the outcome was.
When Exceptions Cannot Be Retried
Not all exceptions are retryable in practice. While our API allows retries for all exception reasons, real-world logistics systems often distinguish between retryable and non-retryable exceptions:
| Category | Reasons | Typical Action |
|---|---|---|
| Retryable | RecipientUnavailable, WeatherDelay | Schedule redelivery |
| Needs Investigation | AddressNotFound, CustomsHold | Contact sender or customs |
| Terminal | DamagedPackage, RefusedByRecipient | Return to sender |
In a production system, you might add a IsRetryable property to the exception reason or maintain a separate configuration that maps reasons to allowed actions. For this course, we keep the logic simple and allow retries for all reasons, focusing on the mechanics of the retry workflow itself.
Operational Monitoring
Operations teams need to see which parcels are currently in exception status so they can take action. A dedicated GET endpoint filters parcels by the Exception status and returns them as a list. This endpoint serves as a dashboard data source for logistics monitoring.
Filtering by status is a simple query:
csharp1var exceptions = await _context.Parcels2 .Where(p => p.Status == ParcelStatus.Exception)3 .ToListAsync();
In a production system, this query would include pagination, sorting by exception age, and possibly filtering by exception reason.
Design Decisions
Several design choices shape our exception handling implementation:
Maximum of 3 attempts. This is a common industry default. After 3 failed deliveries, the cost of additional attempts outweighs the benefit. The parcel is returned to the sender, who can arrange an alternative delivery.
Attempt count lives on the Parcel entity. Rather than counting tracking events to determine the attempt number, we store the count directly on the parcel. This is simpler to query and avoids inconsistencies if events are modified.
Any exception reason allows retry. Our simplified model allows retry for all exception reasons. A production system might restrict retries for certain reasons like DamagedPackage or RefusedByRecipient where re-attempting delivery would not help.
Status validation on transitions. The exception endpoint only accepts parcels in InTransit or OutForDelivery status. The retry endpoint only accepts parcels in Exception status. These guards prevent invalid state transitions.
Summary
In this lesson, you learned:
- Delivery exceptions transition a parcel to
Exceptionstatus with a specific reason - The retry workflow moves a parcel from
Exceptionback toInTransitwith an incremented attempt count - After 3 failed delivery attempts, the parcel auto-transitions to
Returnedstatus - Every exception and retry generates a tracking event for audit purposes
- A dedicated endpoint provides operational visibility into parcels currently in exception status
Next, we will build the exception reporting endpoint with the ExceptionReason enum and status transition logic.