12 minlesson

Exception Workflows Overview

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:

ReasonDescription
AddressNotFoundThe delivery address does not exist or cannot be located
RecipientUnavailableNo one was available to accept the package
DamagedPackageThe package was damaged during transit
WeatherDelaySevere weather prevented delivery
CustomsHoldThe parcel is held at customs for inspection
RefusedByRecipientThe 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 / OutForDelivery
2
3 ▼ (delivery fails)
4 Exception
5
6 ├──► Retry requested ──► InTransit (attempt count incremented)
7 │ │
8 │ ▼
9 │ Next delivery attempt
10 │ │
11 │ ├──► Success ──► Delivered
12 │ │
13 │ └──► Fails again ──► Exception
14
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.

csharp
1public class Parcel
2{
3 // ... other properties
4
5 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:

csharp
1public enum ExceptionReason
2{
3 AddressNotFound,
4 RecipientUnavailable,
5 DamagedPackage,
6 WeatherDelay,
7 CustomsHold,
8 RefusedByRecipient
9}

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 DeliveryAttempted event is created with the exception reason in the DelayReason field 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 Returned event 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:

CategoryReasonsTypical Action
RetryableRecipientUnavailable, WeatherDelaySchedule redelivery
Needs InvestigationAddressNotFound, CustomsHoldContact sender or customs
TerminalDamagedPackage, RefusedByRecipientReturn 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:

csharp
1var exceptions = await _context.Parcels
2 .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 Exception status with a specific reason
  • The retry workflow moves a parcel from Exception back to InTransit with an incremented attempt count
  • After 3 failed delivery attempts, the parcel auto-transitions to Returned status
  • 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.