Our Team for this project:

  • 3 developers
  • 1 project manager
  • 1 UX professional

Our applications support the College of Engineering, mostly outside of the classroom

We maintain 70 legacy applications

developer
NC State University logo
It's harder to read code than to write it. This is why code reuse is so hard.

- Joel Spolsky

Everybody likes to write reusable code, and no one wants to reuse anybody else's code.

- Kurt Koppelman (@moonhead)

Bootstrapping is important

  • We support legacy applications, some very old

  • Updating spaghetti codebases is RISKY

  • Rewriting large legacy applications is TIME CONSUMING

  • Bootstrapping meets you in the middle

Legacy Software

  • Software developed using older technologies and practices
  • It can be difficult to replace because of its wide use.
  • Often a negative term
  • Referencing a system as "legacy" often implies that the system is out of date or in need of replacement.

Spaghetti Code

The relationships between pieces of code are so tangled, it’s nearly impossible to add or change something without unpredictably breaking something.

Refactor

Technique for restructuring an existing body of code, altering its internal structure without changing its external behavior.

Technical Debt

A metaphor referring to the eventual consequences of any system design, software architecture or software development within a codebase.

How to Make Technical Debt

  • Leave a codebase untouched
  • Develop under a tight deadline
  • Let novice programmers build it
  • Maintain by multiple developers
  • Change goals of the app

Bootstrapping (software)

Building onto an existing system for the purpose of improvement with the least amount of sweat equity and development cost in the process.

Survey Your Application

  1. Talk to users of the application

  2. Study the codebase

  3. Examine the new feature requests

Survey Your Application

  1. Talk to users of the application

  2. Study the codebase

  3. Examine the new feature requests

Talk to Users

  1. Is their process consistent with the application?

  2. What are the pain points?

  3. Do they have concerns with the application?

Don’t rely on developer feedback

User Feedback: Process

Process was inconsistent with the application

  • Language of the customer was different from the application
  • Selection Committee used spreadsheet to manage budgets
  • Candidates were added to a spreadsheet
    (Scholarship Account, Student ID, Amount, Term)

User Feedback: Pain Points

Process exited and re-entered the system through spreadsheets

High Margin of Error

User Feedback: Concerns

  • Many awards were rejected by Financial Aid

  • Didn’t trust that the best candidates were being chosen

  • Scoring algorithm was not clear/effective

  • Multiple majors weren't allowed

  • NOT ALL MONEY WAS BEING AWARDED

  • MONEY LEFT UN-GIVEN ⇒ ANGRY DONORS

Survey Your Application

  1. Talk to users of the application

  2. Study the codebase

  3. Examine the new feature requests

Study the codebase

  • Talk to past/current developers

  • Verify functionality does what everyone thinks it does

  • Look for entanglements

Codebase of Scholarships

  • Large App model

    • SQL queries, only slightly dynamic

    • Functions weren’t single-purpose

    • No Bounded Contexts between Students, Selection, and Foundation

Codebase of Scholarship

  • Student application data was a single row in table

    • Academic information wasn’t updated when it changed

    • Major was a single column in that row

Survey Your Application

  1. Study the codebase

  2. Talk to users of the application

  3. Examine the new feature requests

Examine New Feature Requests

  • What new features are needed?

  • How they might be implemented?

New Features: Scholarships

  • Explicit criteria matching, excluded non-matching applicants

  • Current student data, query their GPA, Major, etc at time of selection

  • Students have multiple majors

Before you start

  • Is there another application that can do what it does? Is it better?

  • Is this a worthwhile investment?

  • If so, what are the Most Valuable Features?

Include Users in Decisions

  • Explain why this work is necessary

  • Be open about errors in the application

  • Build trust from the beginning

Set Expectations

  • Customer has an open door to you

    • add work from the side

    • change processes

  • Keep the door open anyway!

Prioritize and Scope Work

  • Bootstrapping grows FAST!

    • require more people to be engaged in the process

    • wear people down over time

  • Scope the work you are agreeing to do

Planning Work for Scholarships

Divided the application based on timeline

we didn't scope the work

Mitigate Risk

© photo of Niamh - acrobat

Stabilize the Code

  • Version control
    • Stabilize the code base and preserves history

     

  • Development and Staging environments
    • No more developing in production!!!!
    • Created fake student data

Test Everything You Need to Keep

  • Acceptance tests stabilize functionality you need to keep

    • Hooked testing interfaces into framework

    • Used Codeception to view contents of the pages

  • Unit and Functional tests for everything you build

Composer

  • Allowed us to use libraries
  • Checked `composer.json` and `composer.lock` into git
  • added `/vendor` to the `.gitignore` file

Composer packages

  • Testing with Codeception, PHPUnit, Mockery
  • Twig templates
  • Pimple container
  • Illuminate database (Eloquent)
  • Phinx for database migrations

Composer Require


{
    "name": "itecs/scholarships",
    "description": "Scholarships application for the College of Engineering.",
    "require": {
        "php": ">=5.3.3",
        "robmorgan/phinx": "*",
        "pimple/pimple": "~3.0",
        "ncsu/auth": "dev-master"
    },
    "require-dev": {
        "codeception/codeception": "2.0.*"
    }
    ...
}

Database Migrations: Phinx

  • Allows you to change DB across environments

  • Gives you power to undo the change if there is a problem

$ vendor/bin/phinx create CreateUserLoginsTable
<?php
    use Phinx\Migration\AbstractMigration;

    class CreateUserLoginsTable extends AbstractMigration
    {
        public function up()
        {
            $table = $this->table('users');
            $table->renameColumn('bio', 'biography');
        }

        public function down()
        {
            $table = $this->table('users');
            $table->renameColumn('biography', 'bio');
        }
    }
$ vendor/bin/phinx migrate
$ vendor/bin/phinx rollback

<?php
use Phinx\Migration\AbstractMigration;

class CreateUserLoginsTable extends AbstractMigration
{
    public function change()
    {
        // create the table
        $table = $this->table('user_logins');
        $table->addColumn('user_id', 'integer')
        ->addColumn('created', 'datetime')
        ->create();
    }
}
$ vendor/bin/phinx migrate
$ vendor/bin/phinx rollback

The Good Stuff

Bootstrapping: Filetree

new code in '/src' alongside the '/app' directory

File tree DDD File tree

Bootstrapping: Namespaces

  • '/src' is given a namespace
  • namespaces are autoloaded in composer

{
    "name": "itecs/scholarships",
    "description": "Scholarships application for the College of Engineering.",
    ...
    "autoload": {
        "psr-4": {
            "ITECS\\Scholarships\\": [ "src/", "app/core/" ],
            "Codeception\\Module\\": "src/",
            "Tests\\Substitute\\": "tests/_helpers/"
        }
    }
}

Bootstrapping: Namespaced Class

This allows us to reference a class like:

ITECS\Scholarships\Common\Services\GPAService

<?php

namespace ITECS\Scholarships\Common\Services;

use ITECS\Scholarships\Common\Values\GPA;
use ITECS\Scholarships\StudentApplication\Domain\Student\StudentId;

interface GpaService
{

    /**
     * @param StudentId $studentId
     *
     * @return GPA
     */
    public function findGpaFor(StudentId $studentId);

}

Events

  • Student Submitted Application

  • Budget Allocated To Scholarship

  • Award Was Given To Student

Dependency Injection

Dependencies

The dependencies are the objects your class needs to function

GPAService needed a DB connection

Inject the Dependencies

Instantiate the dependency as a parameter in the constructor or use a setter


function __construct(Database $database)
{
    $this->database = $database;
}

Dependency Injection

  • Decouples our code from low level implementation details
  • Instantiate our classes with their dependencies
  • AND instantiate those dependencies

Dependency Injection Container

  • A map of dependencies your class needs along with the logic to create those dependencies if they haven't been created yet
  • Can resolve complex dependencies transparently
  • Modular when you need to swap a dependency, only update the container

Pimple

  • Create your container by instantiating the Pimple class

Base Controller


<?php

namespace ITECS\Scholarships;

use \ReflectionClass;
use \Log;

/**
* Base Application Controller Class
*
*/
class BaseController extends \Controller {

    const BAD_REQUEST=400;
    const FORBIDDEN=403;
    const NOT_FOUND=404;
    const METHOD_NOT_ALLOWED=405;
    const SERVER_ERROR=500;

    protected $serverResponseCodes;
    protected $authenticationService;

    /**
    * @var Pimple\Container
    */
    protected $container = null;

    /**
    * Constructor
    */
    public function BaseController()
    {
        parent::Controller();

        // In Ameyrika, BaseController Contains Container
        global $container;
        $this->container = $container;

        $this->authenticationService = $this->container['ITECS\Scholarships\IdentityAccess\AuthenticationService'];

        session_start();

        $this->setExceptionHandler();

        log_message('debug', "Base Controller Class Initialized");
    }
}

'app/services.php'


<?php

    use Pimple\Container;
    use Illuminate\Database\Capsule\Manager as Capsule;

    $container = new Container();

    $container['config'] = require_once('config.php');

    $container['database'] = function ($c) {
        $capsule = new Capsule;

        // PHP 5.3 doesn't do array de-referencing.
        // @todo update for php54
        $config = $c['config'];

        $capsule->addConnection(
            array(
                'driver'    => 'mysql',
                'host'      => $config['db']['default']['hostname'],
                'port'      => $config['db']['default']['port'],
                'database'  => $config['db']['default']['database'],
                'username'  => $config['db']['default']['username'],
                'password'  => $config['db']['default']['password'],
                'charset'   => 'utf8',
                'collation' => 'utf8_unicode_ci',
                'prefix'    => '',
                ),
                'default'
            );

        // Make this Capsule instance available globally via static methods... (optional)
        $capsule->setAsGlobal();

        return $capsule;
    };

Configuration file

  • DB connections

  • Base URL

  • Set paths to twig templates

  • Customize Notice messages

  • Set config variables for services

Configuration file

'/app/config.php'


<?php
    return array(
        /* base url for path in the site */
        'app' => array(
            'base_url' => sprintf('http://localhost:%s/', isset($_SERVER['SERVER_PORT'])
            ? $_SERVER['SERVER_PORT'] : ''),
            'index_page' => 'index.php/',
            'debug' => FALSE
        ),

        /* DB configuration */
        'db' => array(
            'default' => array(
                'hostname' => "local__server",
                'port' => "3306",
                'username' => "uname",
                'password' => "pword",
                'database' => "db_name_"
            )
        ),

        'twig' => array(
            'template_path' => array(
                __DIR__ . '/templates',
                __DIR__ . '/templates/dashboard',
            )
        ),

        'authorization' => array(
            'funding' => array('elstamey', 'person2'),
            'developers' => array('elstamey'),
            'coordinators' => array('person3')
        ),

        'notice' => array(
            'enabled' => false,
            'type' => 'info',
            'headline' => null,
            'message' => null,
            'syslink' => null
        ),

        'errorHandling' => array(
            'emailExceptions' => false
        ),

        'awardLetters' => array(
            'letterhead' => '_itecs/notification-samples/ncsu_coe_aa_letterhead.pdf',
            'academicYears' => '2015-2016',
            'respondByDate' => 'May 11, 2015',
            'templates' => array(
                'awardLetter' => 'layouts/letters/award-letter.twig',
                'awardEmail' => 'layouts/letters/award-email-body.twig'
            ),
            'fixedAttachments' => array(
                '_itecs/notification-samples/thank_you_template.pdf'
            ),
            'signatureImagePath' => '_uploads/tmp/deans_signature.png',
            'outputPath' => '_uploads/awardLetters/'
        ),

        'emailService' => array(
            'sendLimit' => 5,
            'retries' => 3,
            'throttleTime' => 2, // seconds
            'fromAddress' => 'Scholarships Application <engr-webmaster@ncsu.edu>',
            'replyToAddress'=>'College of Engineering <engineering@ncsu.edu>'
        )
    );

?>

We passed this array into a $container['config'] variable

Bootstrapping: Connecting to the Framework

  • '/app/controllers'
    new controllers for the new functionality

  • '/app/services.php'
    defined and configured twig, database, et al
    required in the CI index.php file

  • '/app/bindings.php'
    the roadmap of our new code and dependencies
    required in the '/app/services.php' file

Container

'/app/bindings.php'


<?php

    /* example one */

    use Scholarships\Selection\Services\IlluminateDatabaseGpaService;

    $container['Scholarships\Common\Services\GpaService'] = function($c) {
        return new IlluminateDatabaseGpaService($c['database']);
    };

Container

'/app/bindings.php'


<?php

/* example two */

use Scholarships\Selection\ApplicantQueryService;

$container['Scholarships\Selection\ApplicantQueryService'] = function($c) {
    return new ApplicantQueryService(
        $c['Scholarships\StudentApplication\StudentQueryService'],
        $c['Scholarships\Common\Services\ResidenciesService'],
        $c['Scholarships\Common\Services\GpaService'],
        $c['Scholarships\Common\Services\UnmetNeedService'],
        $c['Scholarships\Common\Services\CandidateQualificationService']
    );
};

Controllers


<?php

class Selectionnext extends BaseController
{
    public function Selectionnext()
    {
        parent::BaseController();

        $this->scholarshipRepository = $this->container['Scholarships\Selection\Scholarship\ScholarshipRepository'];
        $this->collaborationsService = $this->container['Scholarships\Selection\CollaborationsService'];
        $this->committeeService = $this->container['Scholarships\Selection\CommitteeService'];
        $this->authService = $this->container['Scholarships\IdentityAccess\AuthenticationService'];
        $this->awardsService = $this->container['Scholarships\Selection\Scholarship\AwardsService'];
        $this->events = $this->container['Scholarships\Support\Events\EventStore'];
    }

    private function getScholarshipDashboard($scholarshipId,$keepItLight=false)
    {
        $committeeMember = $this->getCommitteeMember();

        $presenter = new ScholarshipDashboardPresenter(
            $committeeMember,
            ScholarshipId::fromString($scholarshipId),
            $this->events,
            $this->scholarshipRepository,
            $this->container['Scholarships\Selection\ApplicantQueryService'],
            $keepItLight
        );

        $scholDetails = $presenter->asArrayForJson();
        $scholDetails['json_string'] = json_encode($scholDetails);

        return $scholDetails;
    }
?>

Summary: Scholarship Wins!

  • Implemented multiple majors successfully
  • Eliminated unqualified candidates
    • made the scoring easier to read
    • reduced the manual review of candidates
  • Selection process was entirely inside the application

Summary: Scholarship Wins!

  • Restored confidence in selection process!
  • Fewer awards were rejected!
  • More Scholarship Money was awarded in the application than ever before!

By May 2015: Approximately $1,074,394 Awarded

Summary: Lessons Learned

Tight deadlines with un-scoped work, we created technical debt that we would have to address in the next academic year

Summary: Lessons Learned

  • Bootstrapping Legacy Apps
  • Event Sourcing
  • Domain-Driven Design
  • Command Query Response Segregation
  • Project Management
  • A LOT!

Summary: Accomplishments

  • Replaced the full Student Application
  • Replaced the Selection Process with improved functionality
  • Built Event-sourced distribution of scholarship money

Old Selection Interface

New Selection Interface

Planning: Year Two

  • Rollover between Academic years
  • The Scholarship CRUD
  • Beginning of the year Fund allocations
  • Increase/Decrease of Funds

University is replacing

What now?

  • Backed up and removed all events from the previous year
  • Minor code improvements
  • Just completed the second year's primary selection window
  • Two years of selection until Academic Works could get up and running
  • Academic Works is online.

Recommended reading

Thank You!

Emily Stamey

Twitter: @elstamey

Joind.in: https://joind.in/talk/30b5f