When you use this web application pattern, you 'lease-out' information, or in other words, reserve a document for use by a single process. By doing so, you manage any conflicts with any other processes that my attempt to access the document. Imagine you want to build an online ticketing system that meets the following rules:
All seats being ticketed are unique; no two seats are the same,
A user can purchase a ticket once the system guarantees a seat,
A user might not complete a ticket purchase,
The ticket should be available to the user at checkout.
To fulfill these requirements, we can use these techniques:
Document Model: Provide one document per ticket.
Lease/Reserve: Implement a lease for tickets. Once a user chooses a seat, we reserve the ticket and a user has 5 minutes to purchase it.
Manage States, and Compensate: A seat can be made available again; expired tickets can be offered once again. If there are failures when a ticket is in an intermediate state, the system can compensate.
The process would look like this if follow the basic application flow:
The initial stage of our ticket document, as JSON, would appear as follows:
{ "ticket_id" : "ticket1", "seat_no" : 100, "state" : "AVAILABLE" }
The ticket document has an unique id, an associated seat, and an
explicit state field to store that state of
our ticket transaction. We can use this information in our
application logic to ensure no other process tries to reserve
the seat. We can also use this information to roll-back the
ticket to an initial state. Imagine a user searches for open
seats, and then they want a seat that is unavailable. Our system
can get all the tickets that were requested but not purchased by
other users; these will all be tickets with expired leases. So
we can also use this information to reserve seats and return
seats to a pool of available seats that we offer to users. If a
user selects a open seat, we put the ticket in their shopping
cart, and indicate this in the ticket document:
{ "ticket_id" : "ticket1", "seat_no" : 100, "state" : "INCART", "expiry" : <timestamp> }
Notice that when we update the state of the ticket, we also
provide an expiration. The expiry in this
case is 5 minutes, and serves as the lease, or time hold that is
in place on the ticket so that no other processes can modify it
during that period. The user now has 5 minutes to pay for the
ticket. If a user moves forward with the purchase, our
application should then get each ticket in the user cart from
Couchbase Server and test that the tickets in the user shopping
cart have not expired. If the ticket lease has not expired, we
update the state to PRE-AUTHORIZE:
{ "ticket_id" : "ticket1", "seat_no" : 100, "state" : "PRE-AUTHORIZE", "expiry" : <updated_timestamp> }
Note at this phase we also update the timestamps to 5 minutes
once again; this provides the additional time we may need to
authorize payment from a credit card, or get an electronic
payment for the ticket. If the payment fails, for instance the
credit card is not authorized, we can reset the tickets to the
state AVAILABLE. Our system will know that
the ticket can be returned to the pool of available tickets that
we present to users. If the payment succeeds, we then set the
ticket state to SOLD and set the expiration
to 0:
{ "ticket_id" : "ticket1", "seat_no" : 100, "state" : "SOLD", "expiry" : 0 }
So we set the expiration explicitly to 0 to indicate the ticket has no expiration since it is sold. We keep the document in the system so that the user can print it out, and as a record until the actual event is over. Here is the process once again, this time we also demonstrate the state changes which keep track of the ticket along with the application flow:
This diagram shows some of the compensation mechanisms we can
put in place. If the seat that a user selects is not
AVAILABLE we can reset all the tickets that
are expired to AVAILABLE and retrieve them
for the user. If the user fails to complete the checkout, for
instance their credit card does not clear, we can also reset
that ticket state to AVAILABLE so that it is
ready to retrieve for other users. At each phase of the user
interaction, we keep track of the ticket state so that it is
reserved for checkout and payment. If the system fails and the
ticket is persisted, we can retrieve that state and return the
user to the latest step in the purchase they had achieved. Also
by preserving the ticket state and expiration, we withhold it
from access and changes by other users during the payment
process.
An alternate approach you can use with this same pattern is to have a ticketing system that offers a fixed number of general admission tickets. In this case, we can use lazy expiration in Couchbase Server to remove all the tickets once the event has already passed.