Process with States

I implemented this again today, and realized it was a pattern that I fall back on regularly.

When dealing with a system with several states, and actions to be performed at each state, I usually break things up into a state diagram.  Inevitably, I end up with a “State” field, the the question becomes what to put in the state field.

Take an example of sending an invite to a user to come in and use an app.

What I do not do:

Alter Table Invitation

add state int not null default(1)

 

enum State { Start = 1, Email = 2, Consumed=3, Expired=4 }

What I do (approximately):

alter table Invitation add
	state varchar(25) null, 
	stateError bit(1) default(0) null, 
	stateTimestamp DateTime default(getdate()) not null

public static class InvitationState { 
   public const string SendEmail="SendEmail";
   public const string WaitInbound="WaitInbound"; 
   public const string InviteConsumed="InviteConsumed"; 
   public const string InviteExpired="InviteExpired";
}

Why?

  • It is my experience that for any flow, a future spec will come up with will add new states in the middle of that flow.  If you go with an int value, you end up with the flow being 1->9->5->6.   By going with a varchar, its much more obvious when doing a select from the db, and it allows for the addition of new states.
    • A previous employer loved using CHAR(4) and state names like “STRT”,”MAIL”,”CONS”,”EXPR” because Oracle stored the Char(4) like an int.   Very similar to Apple’s Resource Fork’s from back in the day.
    • I’m not so worried about the speed difference it makes.  I feel that the flexibility it gives makes up for any performance loss.
  • Either we have an action state, named with a “Verb Noun”, or we have and end state, named with “Noun Verb”.
  • The state names are based on what IS happening or SHOULD happen to the row, not what has already happened, except for the end states.
  • For any action state, if the attempt at performing that action fails, I set the stateError bit to 1.  This lets me know I need to do something to fix that; when its fixed, I can change the stateError back to 0.
    • In large batch systems, I write the console based action-taker with an override switch:  to work on a specific Id, even if the errorState=1; and to do it in a transaction but not commit the transaction.  
    • This lets me do test-things with rows that failed, while the main batch system processes all the other non-errored rows.   Once I get it to working, I tell it to go ahead and commit, and the system continues on its merry way.
  • The system only deals with the states that it knows.  This lets the technical OPS team (whoever that is) use the state field for its out-of band processes, stuff like setting the state to ‘Ken’, for example.   Because Ken is the man who knows what is up with those rows.
  • In really large critical systems, there’s an audit table and a trigger that auto-inserts any changes to the audit table. 

The result of this is, I get Christmas cards in the mail from former clients with phrases like “Still amazed by your code!!!”.   🙂

Hope this helps.