Creating Delightful Developer Experiences

Happy and focused developers write better code

Rajesh Naroth
Level Up Coding

--

There are no ideal software projects. Challenges are plenty, caused by technology and people. But I believe that if you provide a fantastic developer experience, you end up creating a higher quality product. Bugs are a manifestation of lack of knowledge, confusion, short cuts, bad practices and over dependence on horizontal organizations/packages. Here are a few factors that can improve a frontend developer’s day in the life. While these factors may seem like common sense, we still see these compromised, especially as a project ages.

1. Learning curve

Source: https://freesvg.org/by/OpenClipart

How soon can a new developer start contributing to the project? As a frontend developer, can someone with a good grasp of JavaScript, CSS, and HTML get started easily?

Framework

How easy is your framework to master? I like React because of its small footprint. JSX is just JavaScript expressions that embraces composition. In fact, composition is the essence of React.

Angular has a huge learning curve, as it forces you to adopt class based development and learn advanced TypeScript. RX.js has a huge API to master, along with a vast framework documentation. I’d use RX.js as a library when the problem domain demands it. UI is not it.

Vue.js seems simple and has a small API footprint. I believe Vue.js is a comfort zone for those coming from the jQuery world who like keeping JS, HTML, and CSS separate. Yet, as a software engineer, I find it tough to reason string based dependency injection and DSLs.

Composition is the essence of software.

React is just that. A React App is a component that is composed of smaller components. React started off with class based components. Now, with the advent of hooks, you can abandon classes completely. (except for Error Boundaries because there is no hook available to match the componentDidCatch lifecycle method).

With this approach, you can also abandon a huge chunk of ES6 and TypeScript that deal with classes. At HPE, our team not only follows this for our React projects, we also take it a step further to trim down our JavaScript.

Learning curve from dependencies

CRA helps convert React into a framework that solves most of the frontend issues such as SASS, build, etc. However, you may need to adopt a few more libraries such as:

  • A component library
  • Routing
  • i18n
  • Form management
  • State management

IMO React’s useState(), useReducer() and Context API are plenty enough to manage state in a clean way. Do your due diligence to keep dependencies minimal. Some libraries are magical. Magic is dangerous. We use Formik in our projects and it is fantastic. Magical. That is until you try to organize your forms. Formik’s magic adds a steep learning curve when you need to extend it to custom components. It still beats react’s form binding.

Documentation

Other than high level architecture and design docs, the only documentation I recommend are live ones. We use Storybook extensively to demonstrate component usage and layout patterns. For more complex use cases, we create full page samples that demonstrate state management and api integration. Nothing like copy/paste-able working code to speed up your development.

In every project that I have worked on, the docs that live on an internal wiki site become stale after a while. No one has the drive to keep it fresh. No one tracks it. No one misses it.

2. Hit the road running

How soon can a newly on-boarded developer see a working version of the app on his laptop? My goal is this:

  • Install git, node, nvm and vscode
  • Clone the repo
  • npm install
  • Run a couple of cli-s if need to.
  • npm start

But for most projects I’ve worked on in the past, this is not the case. Here are certain things I’ve encountered.

  • I follow a series of steps from a wiki page and hit a roadblock. Now, someone has to send me instructions over email on the extra steps needed. This is called tribal knowledge. It is very common. It is created via quirks in OS dependency, short cuts done to make the app work etc.
  • I set up and run databases and several APIs locally. These steps are not documented properly and often documentation is stale. My only hope is to find someone who has walked this path before to help me.
  • To get some parts of the software running, I have to install Eclipse, modify environment variable and run Java servers. For this to work, someone definitely needs to send you a list of environment variables. For those without Java experience, this is excruciating.

What can you do:

  • Create mock APIs that can run without database or server dependencies. Run it right within CRA. Use a library-like faker to generate data. Keep the APIs as close as possible to the real ones. This also helps to make progress even when the APIs are not ready. If coded to the exact specs, integration is a breeze.
  • Be aware of tribal knowledge. It is the information that resides in an obscure stale wiki, old emails, bashrc files or chat. When you need to take extra steps for your app to run, build that into the task runner.
  • Make it easy for new developers to get started quickly and incrementally learn. No one should have to go through months of training before getting started. Just JavaScript expertise should be plenty enough.

Keep it Simple

Easy to say. Hard to accomplish.

Source: wikimedia

Simplicity is a journey than a destination. It is how you do things on a daily basis. It is the inverse of the amount of distractions you deal with accomplishing your goal. If you need to refer to an API doc consistently, you have dependencies that are complex. If you find your own code alien revisiting it after a few months, your design is overly complex.

Software is complex. Whether it is social media, data management, banking or entertainment, the inherent complexity is unavoidable. The following paper addresses this.

What can be avoided is accidental complexity. This arises from the technology choices and design strategies we make while solving the domain’s problem. As mentioned before, for frontend development, your framework and library choices matter. Here are a few things you can do to tone down accidental complexity:

  • Adopt a simple language. For us, JavaScript is the only choice, but you can further simplify it as mentioned before.
  • Adopt frameworks and libraries with small API footprint. No matter how appealing the tagline is.. Such as “everything is a stream” (Rx.js), it may only add more complexity in most cases.
  • Choose technologies that can be adopted incrementally. We use TypeScript extensively in our projects. Yet, it wasn’t the case when we started. Until we understood it fully and embraced good patterns, it was optional. The learning was incremental. Even now, for Higher Order functions and components, we find it tough to type them fully. Here, instead of tacking on complex typing, we simply use “any” type and the unit tests takes care of their resiliency.
  • Avoid OOP. OOP was not used or implemented how it was meant to be. For front end engineers, J2EE completely screwed it up with EJBs and such. Then there was a push to simplify it with the POJO movement. I have been in projects where a simple project designed by an architect had posted a full wall of UML diagram, explaining his design. Java may be great for server side but was a horrible choice for frontend. GWT anyone?
  • Even in the past decade where the power of JavaScript as a functional language was revealed, the TC39 ECMA foundation keeps pushing half-baked class paradigms into JavaScript.
  • Use composition as the essence to your design. Avoid class based inheritance. Use functional patterns such as compose, map/filter and reduce for data transformations and state management. Use a library such as ramda.js to provide object and list management utilities.

Programming practices

We talked about taking a minimalist approach to JavaScript and adopting functional patterns. Sticking to the following practices can keep the code less buggy and developer experience better:

Practice immutability

  • Prefer const over let. This is the first level of immutability that is effective for non-object types.
  • Use spread operator to create new copies instead of mutating an existing object. Use deep cloning if you need to.
  • Use reducers/actions for state management. A reducer used for managing state is a pure function that, given the current state and an action, returns a new state.
  • Learn how to write pure functions. Do not mutate function arguments or anything outside its scope. Learn how to unit test pure functions well.

Write idiomatic code

  • Some developers get high writing clever code, and I was one of them. Clever code is just that. Clever. It is tough to reason about. Avoid clever code and call it out when you see them in code reviews.
  • Use a linter and integrate it with git hooks. Use an opinionated formatter such as Prettier. Enforce conventions such as variable names and spacing on top of this via code reviews.

Use static typing

TypeScript is intimidating. The documentation pretends that it is its own language. However, we adopted a minimalistic approach to it by abandoning class based development. After this, TypeScript became our friend. We mostly use TypeScript for:

  • Function signatures
  • Component Prop Types
  • State and local variables

HOCs and dynamic behavior are really tough to type. We shamelessly use “any” type without wasting hours googling on “how to type”. Remember, TypeScript is just a tool. Don’t treat it like a dogma.

Unit tests

  • Have enough demonstrable unit tests. This helps new developers learn how to write assertions for helper functions and components. Jest assertions and snapshots are wonderful.
  • If you find yourselves injecting a mock into a unit test, it may be time to move that into an integration or acceptance test. Here is a recommended read.
  • Writing software by composing unit testable functions and components is an acquired skill. When you build software composed of resilient units, the overall project will reflect that.

Acceptance tests

  • Acceptance tests are integration tests performed within a functional scope. Functional tests are traditionally done by QA using Java based Selenium. Using frameworks such as cypress and testcafe, developers can write isolated acceptance tests in JavaScript.
  • In our team, QA leverages this further more by extending the scenarios in their test suites. This eliminates much of the testing overlap between dev and QA.

Bus factor

Image Source: Pixabay

The “bus factor” is the minimum number of team members that have to suddenly disappear from a project before the project stalls due to lack of knowledgeable or competent personnel. In software development you can minimize this by taking some measures.

  • No Heroes. Heroes are amazing engineers who would resolve issues single handedly without blinking an eye. They are hard workers and experts of the domain and project. However, when a hero leaves a project, it leaves a huge hole. Two weeks of recorded TOI session are not going to stop the impact.
  • The same point again: minimize the project complexity. Make sure that your project can be developed and maintained by engineers with a wide range of capability. You should be able to onboard new developers quickly. If you project requires deep knowledge of stuff, make sure that there are enough people that have that.

Development in isolation

This is a high goal. Ability to write code in isolation means that:

  • You understand the problem you are solving clearly.
  • You do not have any dependency on anyone. You are not blocked.
  • You can be in the flow. Writing and refactoring as you develop.
  • Merge conflicts are far and few because you are not sharing that folder with anyone.
  • The risk of regression is low.
  • You can write self contained unit and acceptance tests.

I call this a high goal because it depends on how isolated your features are designed. If every feature in your app needs you to work on common sections such as updating routes, metadata etc, there is constant conflict.

Deleting features

Most projects only increase in size. Deprecated functionality aka dead code lingers for ever because of regression risk. Ideally, deleting a feature should be as simple as removing a folder and its routes. This is a great goal to shoot for. It means that you can enhance, refactor or remove features without regression. You could even do customer tests for two radically different versions of the same feature.

Composition as a micro frontend architecture.

Applets, Flash and iframes are micro frontends. But all of them are dead (iframes are used as hacks usually). There are a few blogs you can find about SPA micro frontends. The goal is to develop a container that can host loosely coupled pieces of software. It is a worthy quest. The solutions are still very custom and complex because it tries to solve it within a broad scope. This demand usually occurs when different teams want to deploy their apps in the same “portal”. For example, Angular and React apps living side by side in the same browser tab, which introduces unwanted complexity in your project.

We, at HPE (in our team), discussed this. We decided not to shoot for this overarching goal of mixing frameworks, but instead aimed for an isolated feature development model within React. There was no need to combine JavaScript frameworks. Our platform became a combination of:

  1. Core functionality such as npm tasks, storybook, common components, themes and helpers
  2. A set of isolated features. Global state such as user info, routing etc. are injected into the features containers.
  3. App shells that could create different personas based on the platform it was deployed in. A persona would simply create an experience by picking certain features and compose them within an App shell.

The project was front loaded with #1. After a couple sprints, #2 and #3 were all we did. The developer experience was fantastic. By applying various principles as explained in this blog, we were able to easily scope and assign individual features to developers. On boarding a JavaScript engineer took less than 2 weeks. In about 5 months, we had churned out over 30 features with a team of 13 frontend engineers. The project went from zero to 1400 typescript files and 80K lines of code, unit and acceptance tests included. Hurray!

How to keep DX delightful

Keeping DX delightful is a constant battle. Periodic tech sessions and nit picky code reviews are a must. You have to be aware of the following:

Bad programmers

Like it or not, you will onboard less than ideal programmers in to your team. They will write bad code. Especially in the beginning. You will need to encourage them to follow the core principles and push them to write idiomatic code. With isolated feature development, you can mitigate the risk. Bad code can be refactored without regression.

Code Smells

“A code smell is a surface indication that usually corresponds to a deeper problem in the system.” ~ Martin Fowler

JavaScript is a flawed language. It was created in a hurry with constructs added to compete with Java. Code smells are plenty here. For example, in our React projects, using let is a code smell. Imperative data transformations are not allowed. Being aware of code smells is very key. You enforce it as a team during code reviews.

Normalization of Deviation

Googling the term will give you an idea about what it means and how it applies to different domains.

For example, in an ongoing project, suddenly there is a timeline shift and urgency. Unit tests take a back seat because developers are pressed for time. Thus, they approach it as “make it work and leave it to QA”. The backlog is now is a normal. There is really no “urgency” to fix it since everything seems ok. But this is a ticking time bomb that will manifest itself in the worst possible way soon.

Even small undesirable deviations such as variable name conventions can get picked up and create non idiomatic code. It is only a matter of time before someone will try and access document, window or localStorage variable without discipline. If you’re not able to catch and tame it on time, in a couple years, that practice will spread. Guaranteed.

New dependencies

As a project ages, new dependencies will arise. It is important to pick packages that have a large contributor base and good online presence. Make sure that the packages will seamlessly upgrade with others. Using open source in a fast moving tech is rife with issues such as is-promise, core-js, left-pad and event-stream. Frankly, adopting single function packages like is-promise is ridiculous. Is it that tough to write a simple function called isPromise()?

Refactoring

IMO, refactoring should never stop. I am always in a constant quest to simplify the software. Sometimes, it is using downtime to audit your app for code smells and complexity. Sometimes it is as simple as finding a better name for a function or file name. Refactoring is like maintaining your car. Do it regularly, and it will last you a long time. You do not want your product to become a “legacy” app that developers can’t wait to rewrite.

Code Reviews

Code reviews must be a positive experience. Reviews tend to be impersonal because of its textual nature. Comments must be grounded in empathy and politeness and should enforce shared ownership of the code base. A code review is not a display of cleverness or coding muscles, it is about building a project together. In our team, we encourage everyone to participate in reviews. It helps to keep best practices going and keep the code idiomatic.

Upgrades

Npm packages become stale with in weeks if not months. Frameworks are notorious for introducing breaking changes. Keeping up with stable versions will help keep your product fresh.

Shiny old things

Every now and then, a shiny new solution will rise in the npm horizon. To create products that last long, resist the urge to be an early adopter. Adopt shiny old things.

Evolve

Tools will change. New philosophies will emerge. Frontend solutions are still complex, but the pace of innovation is fast and furious. We have to constantly adapt and evolve.

--

--