I’ve been working on a new talk to describe a project where we are replacing an application that used a complex list of statuses to describe where a student’s enrollment request is in the process.  Examining older applications like this one, you easily see how they closely mimicked the old paper form processes.  The status of the paper form was directly tied to its being added to a stack of papers.  Over time, the approval process became more complex, and the number of piles or statuses increased.  The status drop-down in this application became REALLY long!  Before we begin examining a lot of statuses, I’d like to discuss the simple example.  After a Student submits their Enrollment Request, an Admin either Approves or Rejects their Request.

Bounded Contexts

Our team has built applications using DDD.  We recognize the bounded contexts of Student and Admin spaces in the code as well as in the user interfaces.  Without making this overly complicated, we know that there are some actions a Student makes in this system and at a point, they are just waiting to hear back from the Admin.  There are a completely different set of things the Admin does in this space.  When we create a bounded context, we let the code mirror their real-life processes, without muddying the concerns of the other bounded context.  There is no scenario where a Student can Approve or Reject an Enrollment Request, so we will begin there.

Events to Approve and Reject an Enrollment

A diagram showing a form arrives and is either approved or rejected

A student Submits an Enrollment Request and then waits to hear what has happened to it.  The Admin may request more information from the Student before they can approve or reject.  But on a very basic example, the student submits an Enrollment Request and waits for a notice of Approve or Reject.  The Approve or Reject Events will trigger a message to the student and probably some other things.  Our Admin receives the request and clicks a button, either Approve or Reject.  When they click, a corresponding event is created.  The event is a class like this:

public class EnrollmentRequestApproved implements Event
{
    // @var UUid
    public $enrollmentRequestId;

    // @var StudentId
    public $studentId;

    // @var CourseOfferingId
    public $courseOfferingId;

    public function __construct(Uuid $enrollmentRequestId, StudentId $studentId, CourseOfferingId $courseOfferingId)
    {

        $this->enrollmentRequestId = $enrollmentRequestId;
        $this->studentId = $studentId;
        $this->courseOfferingId = $courseOfferingId;
    }
}

All Events are stored in an Event Store, and they are stored in the order they are recorded.  We can listen for Events arriving in the Event Store, and trigger certain things to happen based on those Events.  Let’s say the Admin wants a list of all Enrollment Requests that haven’t been Approved or Rejected, so they can enroll the students in classes they need.  We’ll call this Enrollment Request Projector.  It will maintain a list of all Enrollment Requests.  When a student submits their request for enrollment, our Projector will add it to the list of Enrollment Requests.

The Admin Dashboard will display all requests in this table. The beforeReplay() and afterReplay() methods allow us to easily rebuild the Projection based on new information.  The apply… methods contain the changes that are made to the table based on the Event.A new StudentRequestedEnrollment event will add a Enrollment Request with a status of “requested”.  But when the admin approves or rejects the request, that corresponding Event will be created.  When our Projector detects the Approve or Reject event, the corresponding Enrollment Request will be removed from the table.

class EnrollmentRequestProjector extends SimpleProjector implements Replayable
{

    /**
     * @var Connection
     */
    private $connection;

    /**
     * @var string table we're playing events into
     */
    private $table = 'proj_enrollment_requests';

    public function __construct(Connection $connection)
    {
        $this->connection = $connection;
    }

    public function beforeReplay()
    {
        $builder = $this->connection->getSchemaBuilder();

        $builder->dropIfExists('proj_enrollment_requests_tmp');
        $builder->create('proj_enrollment_requests_tmp', function (Blueprint $schema) {
            $schema->string('id');
            $schema->string('student_id');
            $schema->string('course_offering_id');
            $schema->string('status');

            $schema->primary('id');
        });

        $this->table = 'proj_enrollment_requests_tmp';

    }

    public function afterReplay()
    {
        $builder = $this->connection->getSchemaBuilder();

        $builder->dropIfExists('proj_enrollment_requests');
        $builder->rename('proj_enrollment_requests_tmp', 'proj_enrollment_requests');

        $this->table = 'proj_enrollment_requests';
    }

    /**
     * @param StudentRequestedEnrollment $event
     */
    public function applyStudentRequestedEnrollment(StudentRequestedEnrollment $event)
    {
        $enrollmentRequest = new EnrollmentRequest;
        $enrollmentRequest->setTable($this->table);

        $enrollmentRequest->id = $event->enrollmentRequestId->toString();
        $enrollmentRequest->student_id = $event->studentId->toNative();
        $enrollmentRequest->course_offering_id = $event->courseOfferingId->toNative();
        $enrollmentRequest->status = 'requested';

        $enrollmentRequest->save();
    }

     /**
     * @param EnrollmentRequestApproved $event
     */
    public function applyEnrollmentRequestApproved(EnrollmentRequestApproved $event)
    {
        $enrollmentRequest = EnrollmentRequest::where('id',  $event->enrollmentRequestId->toString())
        $enrollmentRequest->delete(); 
    }


     /**
     * @param EnrollmentRequestRejected $event
     */
    public function applyEnrollmentRequestRejected(EnrollmentRequestRejected $event)
    {
        $enrollmentRequest = EnrollmentRequest::where('id',  $event->enrollmentRequestId->toString())
        $enrollmentRequest->delete(); 
    }
}

This is a long piece of code to review.  But I hope that it will demonstrate how the events work.  I am still doing quite a bit of hand-waving around the event store and some of our classes that we are using in this project.  I apologize for that.  The part I want you to understand, the useful take-away is that these events can be applied in different contexts of the application.  For the Admin Dashboard, the EnrollmentRequestRejected and EnrollmentRequestApproved has one effect on the Enrollment Request projection.  But if we wanted a view of the Closed Enrollment Requests, we could apply those same events in an opposite way.  Likewise, the Student Dashboard has its own sets of methods that apply the events that are relevant.  From this context, the events are applied differently.  A Student wants to see the status of their requests, and their aim is to be Enrolled, which is actually a step beyond Approved.  An Event tied to the Registration System will let us know that they are enrolled in the course, and completes this part of the example.