DEV Community

Werner Echezuría
Werner Echezuría

Posted on

Practical Rust Web Development - State Machine

Rust's type system allows implementing state machine in a straightforward way. In our application we might use it to define a sale's state in a specific point in time. We can define 4 different states for a sale:

Draft: An user can edit a sale, this can be used as some form of budget or presale, it does not affect inventory or accounting, you can only approve a draft sale.

Approved: The sale can't be edited, now it's an invoice that should be delivered to the client. You can cancel, pay or partially pay an approved sale.

Pay: An user payed the sale, now you can generate a collection receipt for the sale. You can only cancel a payed sale.

Partially Pay: The user received a part of the total payment, this could be used if you want to sell a product by parts using some form of credit, then you generate a collection receipt. You can cancel and pay a partially payed sale.

Cancel: This is an annulled invoice, if you commit a mistake with a sale, you need to generate a credit/debit note afterwards. This is the final state, once you cancel a sale, you can't change its state.

Let's see it in code. Take a look at src/models/

use crate::models::sale::Sale;

#[derive(DbEnum, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum SaleState {

pub enum Event {

impl SaleState {
    pub fn next(self, event: Event) -> Result<SaleState, String> {
        match (self, event) {
            (SaleState::Draft, Event::Approve) => Ok(SaleState::Approved),
            (SaleState::Approved, Event::Pay) => Ok(SaleState::Payed),
            (SaleState::Approved, Event::PartiallyPay) => Ok(SaleState::PartiallyPayed),
            (SaleState::Approved, Event::Cancel) => Ok(SaleState::Cancelled),
            (SaleState::Payed, Event::Cancel) => Ok(SaleState::Cancelled),
            (SaleState::PartiallyPayed, Event::Cancel) => Ok(SaleState::Cancelled),
            (SaleState::PartiallyPayed, Event::Pay) => Ok(SaleState::Payed),
            (sale_state, sale_event) => Err(format!("You can't {:#?} from {:#?} state", sale_event, sale_state))

The enum SaleState is used as a database enum, let's take a look at the migration migrations/2019-09-25-114234_add_state_to_sales/up.sql:

CREATE TYPE sale_state AS ENUM ('draft', 'approved', 'partially_payed', 'payed', 'cancelled');
ALTER TABLE sales ADD COLUMN state sale_state;
UPDATE sales SET state = 'approved'; 

Thanks to diesel-derive-enum crate we can map a Rust enum to db enum.

In order to make sure we are respecting the rules we already defined, we might add a function in src/models/

    fn set_state(context: &Context, sale_id: i32, event: Event) -> FieldResult<bool> {
        use crate::schema::sales::dsl;
        use diesel::ExpressionMethods;
        use diesel::QueryDsl;
        use diesel::RunQueryDsl;

        let conn: &PgConnection = &context.conn;
        let sale_query_builder = dsl::sales

        let sale = sale_query_builder.first::<Sale>(conn)?;
        let sale_state =;



We can add a filter to updateSale function, to make sure we only edit draft sales:

            let sale = diesel::update(

Then we add the approve, pay, partially pay and cancel functions:

    fn approveSale(context: &Context, sale_id: i32) -> FieldResult<bool> {
        Sale::set_state(context, sale_id, Event::Approve)

    fn cancelSale(context: &Context, sale_id: i32) -> FieldResult<bool> {
        //TODO: perform credit note or debit note
        Sale::set_state(context, sale_id, Event::Cancel)

    fn paySale(context: &Context, sale_id: i32) -> FieldResult<bool> {
        //TODO: perform collection
        Sale::set_state(context, sale_id, Event::Pay)

    fn partiallyPaySale(context: &Context, sale_id: i32) -> FieldResult<bool> {
        //TODO: perform collection
        Sale::set_state(context, sale_id, Event::PartiallyPay)

Full source code here

Top comments (1)

alexeyyunoshev profile image
Alexey Yunoshev

Thank you