Step away from frameworks like React or Express to Business-Driven

Step away from frameworks like React or Express to Business-Driven

  • 1276

step away from frameworks like React or Express to Business-Driven ... If you like the fundamentals of business-driven applications, how about

For me, building applications is like playing strategy games. I get to architect complex systems and, in a way, I feel like an artist.

Throughout my career, tight deadlines and ever-changing requirements were constants that often enough left me frustrated and overworked.

I wanted a way to avoid that small, innocent-looking task turning into days of hard work!

A few years ago, I started a quest to distill a better way of engineering software, one that delivers value while keeping me from burning out.

Continuing from my last article, The 4 Layers of Single Page Applications You Need to Know, together with a lot of lessons learned from Robert C. Martin’s Clean Architecture and Eric Evan’s Domain Driven Design book, that quest led me to business-driven applications.

Right now, you’re probably wondering what they are and if it’s worth learning more about them.

The gist of business-driven applications

Whenever business people talk to me about engineering their software, the terms they use are solely related to their business domain and needs. I have rarely seen a client talking about JavaScript frameworks. The software we develop has to represent our customer’s domain.

In today’s market, the goals of our clients will always change together with their software. Clean, precise, long-lasting specifications are nearly impossible. We need to make sure that our software satisfies our partner’s immediate and long term objectives by being fast to implement and easy to change.

In business-driven architecture, everything apart business logic represents low-level implementation.

I feel we are focusing too much on the frameworks and libraries we use. We even call ourselves React or Angular “5. whatever” developers. When asked to describe the applications that we’re working on, we will describe fancy React-Redux apps, forgetting to mention what our applications do altogether. The frameworks and databases we use represent just delivery and persistence mechanisms.

I firmly believe we should visualize applications as ecosystems, composed of core business logic with various components orbiting around it.

This is image title
The Cory Way

If you like the fundamentals of business-driven applications, how about building one together with me?

The loan calculator

Let’s take a hypothetical example of implementing a loan calculator, a simple application to determine the monthly payment for a fixed-rate loan.

With a new client and a short deadline (three one week sprints), we have a very challenging task on our hands.

The first thing we need to do is talk to the customer and understand their business and requirements.

With this priceless information, following the guidelines found in the brilliant book Writing Effective Use Cases written by Alistair Cockburn, we are ready to define the use case for our task (our user story).

Calculate loan use case

Primary actors

  • User
  • Calculator

Main success scenario

  1. The User submits the data necessary to calculate the loans monthly payment
  2. The Calculator validates the data
  3. ~ determines the monthly payment
  4. ~ saves the calculation for later use
  5. ~ sends an email to the User with the calculation
  6. ~ presents the calculation to the User

Extensions

  • Submitted data is incomplete:
    • the Calculator requests the missing data
    • the User supplies the missing data
  • Submitted data is invalid:
    • the Calculator informs the User about the invalid data
    • the User submits valid data
  • The save operation fails:
    • the Calculator returns the calculation to the User and informs him about the error
  • Sending the email fails:
    • the Calculator returns the calculation to the User and informs him about the error

Necessary user data

  • Email
  • Loan amount
  • Loan term
  • Life insurance opt-in

Loan conditions

  • Min. loan amount: $1,000
  • Max. loan amount: $10,000
  • Min. loan term: 1 year
  • Max. loan term: 5 years
  • Interest rate: loan term ≤ 2 years ? 20% per annum : 15% per annum
  • Interest rate discount for life insurance opt-in: -3% per annum
  • The monthly payments will be calculated using this simple formula

Sprint one, the core implementation

We’re not concerned about frameworks, persistence, or email services. We’re business-driven, and we want to create the application’s core.

Using TDD, we have built the loan calculator in TypeScript as an independent node package without any hard dependencies and no delivery mechanism apart from unit tests.

Let’s look at the calculateLoan use case implementation.

export const calculateLoan = async (request: CalculateLoanReq): Promise<CalculateLoanRes> => {
  const { lifeInsuranceOptIn } = request;

  const emailAddress = new EmailAddress(request.emailAddress);
  const loanAmount = new LoanAmount(request.loanAmount);
  const loanTerm = new LoanTerm(request.loanTerm);
  const interestRate = new InterestRate(loanTerm, lifeInsuranceOptIn);
  const monthlyPayment = new MonthlyPayment({
    interestRate,
    loanAmount,
    loanTerm,
  });

  const calculation = new LoanCalculation({
    emailAddress,
    loanAmount,
    loanTerm,
    lifeInsuranceOptIn,
    interestRate,
    monthlyPayment,
  });

  await Context.loanRepo.save(calculation);

  const response: CalculateLoanRes = {
    emailAddress: calculation.emailAddress.value,
    loanAmount: calculation.loanAmount.value,
    loanTerm: calculation.loanTerm.value,
    lifeInsuranceOptIn,
    interestRate: calculation.interestRate.value,
    monthlyPayment: calculation.monthlyPayment.value,
  };

  const email: string = Context.emailServ.generateEmail(response);

  await Context.emailServ.send(email);

  return response;
};

calculateLoan.ts

“calculateLoan” use case

The Calculator receives a request from the User to calculate the loan. It then saves the data through a repository and sends an email with the calculation to the User’s email address, returning it at the end.

I would argue that the implementation reads like the user story.

Even without a delivery mechanism in place, we can use the application through unit tests.

The loan calculator without any delivery mechanism except for tests

Given a loan amount of 10,000 $ to be paid back over 5 years, the monthly payment will be 222.44 $.

describe('calculateLoan', () => {
  let request: CalculateLoanReq;

  beforeEach(async () => {
    request = {
      emailAddress: '[email protected]',
      loanAmount: 10000,
      loanTerm: 5,
      lifeInsuranceOptIn: true,
    };
  });

  it('should calculate the monthly payment', async () => {
    const calculation = await calculateLoan(request);

    const expectedInterestRate = 12;
    const expectedMonthlyPayment = 222.44;

    expect(calculation.interestRate).toEqual(expectedInterestRate);
    expect(calculation.monthlyPayment).toEqual(expectedMonthlyPayment);
  });
});

calculateLoan.test.ts

“calculateLoan” test

The loan calculator depends on a hypothetical loan repository and email service implemented by its consumer, the testing framework, using in-memory solutions.

This is image title
The loan calculator’s dependency contracts

export interface LoanRepo {
  findOne(address: EmailAddress): Promise<LoanCalculation | undefined>;

  save(calculation: LoanCalculation): Promise<void>;
}

LoanRepo.ts

import { CalculateLoanRes } from '../useCases/CalculateLoanRes';

export interface EmailServ {
  send(email: string): Promise<string>;

  generateEmail(calculation: CalculateLoanRes): string;
}

EmailServ.ts

The email service

export class InMemoryLoanRepo implements LoanRepo {
  readonly calculations: LoanCalculation[] = [];

  findOne(address: EmailAddress): Promise<LoanCalculation | undefined> {
    const predicate = (c: LoanCalculation) => c.emailAddress.value === address.value;
    const calculation = this.calculations.find(predicate);

    return Promise.resolve(calculation);
  }

  save(calculation: LoanCalculation): Promise<void> {
    this.calculations.push(calculation);

    return Promise.resolve();
  }
}

InMemoryLoanRepo.ts

The in-memory loan repository

For more information about dependency-inversion, check out my article Practical Inversion of Control in TypeScript with a Functional Twist.

Sprint two, the Express app

To save time, we decide to deliver the first version of the loan calculator through an Express app via Postman.

Our client will be able to test the application’s logic through Postman and give us valuable, early feedback.

Context.initialize({
  loanRepo: new InMemoryLoanRepo(),
  emailServ: new InMemoryEmailServ(),
});

const schema = Joi.object({
  emailAddress: Joi
    .string()
    .email()
    .required(),
  loanAmount: Joi
    .number()
    .min(LoanAmount.min)
    .max(LoanAmount.max)
    .required(),
  loanTerm: Joi
    .number()
    .min(LoanTerm.min)
    .max(LoanTerm.max)
    .required(),
  lifeInsuranceOptIn: Joi.bool(),
});

const loanRouter = express.Router();

loanRouter.post('/calculate', async (req, res) => {
  try {
    const { error, value } = Joi.validate(req.body, schema);

    if (error) {
      res.status(BAD_REQUEST).send(error);

      return;
    }

    const calculation: CalculateLoanRes = await calculateLoan(value);

    res.send(calculation);
  } catch (e) {
    res.status(INTERNAL_SERVER_ERROR).send(e);
  }
});

export { loanRouter };

loanRouter.ts

The loan router

Sprint three, the React app

We decided on delivering the loan calculator through a React application.

The app will import the calculator module and build a fancy UI around it.

We will implement the loan repository and email service using the solutions already put in place in the previous sprint and call them through Ajax requests.

export const LoanCalculator: FC = () => {
  const [loanCalculation, setLoanCalculation] = useState<CalculateLoanRes | null>(null);

  const onSubmit = async (values: LoanCalculatorFormValues) => {
    const {
      email,
      loanAmount,
      loanTerm,
      lifeInsuranceOptIn,
    } = values;

    const calculation: CalculateLoanRes = await calculateLoan({
      emailAddress: email,
      loanAmount,
      loanTerm,
      lifeInsuranceOptIn,
    });

    setLoanCalculation(calculation);
  };

  return (
    <>
      <h1 className='mt-5'>Loan calculator</h1>
      <hr/>
      {
        loanCalculation
          ? <Calculation calculation={loanCalculation}/>
          : <LoanCalculatorForm submit={onSubmit}/>
      }
    </>
  );
};

loanCalculatorComponent.ts

“LoanCalculator” component

It feels like we have to do more, but we did it!

We built our first business-driven application

We created a flexible loan calculator served, depending on the project requirements, both server and client-side. We didn’t tie ourselves to any fancy framework or complicated services.

Starting from the client’s business needs made us very flexible in choosing our technologies. We were able to build loosely coupled software that’s easy to change and maintain.

I believe developing business-driven applications will keep us safe from most difficulties encountered in our day to day engineering work.

If I’ve got this right, we’ll meet on a beach somewhere sipping Pina Coladas! :)

I would love to read your opinions about this article, so please comment below; follow me here or on Twitter to get updates about my work.

The code for this article is at my GitHub repo.