Architectural Decisions and Insane Productivity Boost

  2017-11-20


Approximately a year ago I got a new job and took a lead of a four person development team. The team was working on an online application that was used by governments and local businesses to coordinate import of materials into the country. Since my joining we went through a full refactoring of the old architecture and came up with something remarkable. This post will describe some of the key elements of our new architecture and will highlight the benefits we’ve had from it.

To give some background, the app is a single page web app, built with angular and running asp.net in the backend.

Command pattern architecture

One of the first decisions we’ve made is to use MediatR, meaning that 100% of our business logic now was in the IRequestHandler classes. This made sure that we had a uniform way to invoke any business action by simply doing this:

mediator.Send(new SomeBusinessAction.Request())

Just because every single business action implemented the same interface we could start doing interesting things.

1. Decorators

The first benefit of command pattern architecture is that now we could start using decorators. This allowed to easily implement cross-cutting concerns. We used it to implement

  • audit logging
  • error logging
  • security checks

2. Single HTTP endpoint

Secondly a little less obvious, we could now have a single HTTP endpoint for all of our business actions:

HTTP POST /api/do/SomeBusinessAction

This means that we could get rid of almost all of our asp.net controllers. In our case - we removed around 30% of our asp.net code.

3. We realized that UI can be generated

The next thing we realized is that every HTTP endpoint is nothing but a function and like any function it has inputs and outputs. We started noticing that most of the UI very closely resembles those inputs and outputs.

That meant that we can generate a lot of the UI on-the-fly, based on

  • inputs (HTTP POST/GET parameters)
  • outputs (HTTP response)

To make this possible we needed 2 things:

  • metadata for each HTTP endpoint, describing how to render its inputs and outputs
  • top-level UI control which can generate UI based on the supplied metadata

In its simplest form the UI control would be implemented as something like this:

<form action="/api/do/{{form.id}}" method="POST">
    <label ng-repeat="i in form.inputs">
        {{i.label}}:
        <custom-input metadata="i" />
    </label>
    
    <button type="submit">Submit</button>
</form>

<div ng-repeat="x in form.output">
    <span class="label">{{x.label}}:</span>
    <custom-output value="x.value" metadata="x.metadata" />
</div>

Fast-forward 1 year

This became the start of our 1-year journey to build what we now call UI Metadata Framework. UI Metadata Framework is basically a communication protocol describing client-server communication. UIMF client can be very thin and dumb. It just needs to be able to generate UI based on some metadata. It doesn’t need to have any knowledge of the business whatsoever.

This approach brought us to experience some of the biggest productivity gains I’ve ever seen coming from a single piece of tech. We suddenly almost never had to worry about implementing UI. Most of our UI was generated.

The only time that we needed to write UI code is if needed to implement some new UI control. Once implemented in the frontend, we could then use this UI control directly from our backend code.

For example, let’s say we needed to implement a UI control to display a date in a nice format. We would create a new control which would normally consist of 3 files:

  • DateView.html
  • DateView.js
  • DateView.css

We would then instruct our client that whenever it needs to display date, to use the DateView control. This means that whenever server sent dates, the client would know to use DateView control.

In our particular app we have more than 1000 different pages/screens, and we have approximately 80 different UI controls. Cool thing is that we no longer need to create HTML+JavaScript file for each of the 1000 pages. UI for individual pages is now generated on the fly, during runtime.

Browser testing

One huge advantage of auto-generated UI is that it is uniform and 100% predictable. This makes writing Selenium tests a piece of cake. This realization led us to implement a small wrapper library around Selenium, which gives us friendly API for testing our app. This is how our typical Selenium test looks like:

// `tester` is our custom wrapper around Selenium
// which understands our UI and provides friendly API
// to navigate and test our app.
var tester = require("../config").tester;

describe("admin", function () {
	beforeAll(function () {
		tester.login("admin@example.com", "123456");
	});

	it("can find existing user", function () {
		tester.open("Users")
			.run({ Id: 2 })
			.table("Results")
			.row(1)
			.col(1)
			.hasText("John");
	});

	it("can invite new user", function () {
		var timestamp = new Date().valueOf();
		var email = "jack_" + timestamp + "@example.com";
		var name = "Jack " + timestamp;

		tester.open("Users").action("SendUserInvitation", {
			Email: email,
			Name: name
		});

		tester.open("SearchUserInvitations")
			.run({ Email: email })
			.table("Results")
			.row(1)
			.col(2)
			.hasText(name);
	});
});

If you’ve ever done browser testing, you would appreciate the high level of abstraction of the test above. Writing tests this way gives us 2 advantages:

  • we focus only on the business logic and not on the nitty-gritty details of the client implementation
  • by simply replacing tester object we can repurpose the same test for another client (e.g. - Android app)

Future work

We are still in the early days of this journey, however we have now proven to ourselves that this architecture works and it should work even for very complex apps with elaborate UIs. We are currently actively working on standardizing the UI Metadata Framework protocol and also on implementing clients for various platforms including web, mobile and desktop. I personally strongly believe that this architecture is the future of application development, especially for line-of-business apps.

22bugs.co © 2017. All rights reserved.