One of the challenges of working as a developer is moving from junior to senior. This means looking beyond implementation and starting to think about design and architecture.
Design involves two skills: 1. creating/discovering/considering goals and problems, 2. constructing approaches/solutions.
In this article, we will be looking at some of these most core considerations.
Consider all stakeholders
Before a single line of code is written, it’s important to think about your product from multiple perspectives to create the right concepts and abstractions.
Good code is much more than just functioning code. The quality of a code base is only as good as it serves the people it touches. Generally, there are three major stakeholders for most products: the end users, the product people, and the developers. These are the places you can derive most of your goals.
End users and end product
A deep understanding of the end user and product is needed to make good decisions about your front-end system. End-user design is generally created by a combination of UX designers, graphic designs, and product management (including marketing). While it may be up to other people to make the major decisions about the design, understanding the end product will help you figure out the project requirements and make decisions about designing the entities/data structures.
Over the last month, my company (Journy) has been working on a tool for building flexible quizzes. Our core product is the design and creating of custom itineraries for travelers who want to have authentic experiences without the difficulty and stress related to researching and booking every aspect of the trip. As the first step, we have customers take an on-boarding questionnaire to help our trip designers design the perfect trip. This means asking about interests, travel dates, price points, reason for traveling, etc.
Discover important requirements
End users and the product help with understanding the required functionality. Understanding the product as a whole makes designing your architecture much easier. Since you are going through this process, try and think about how you would implement each part and consider any challenges and problems. For example, one important feature for our app was conditional rendering: changing what is displayed based on how the user responds.
This feature was showing content based on the user’s answers. If we didn’t think about this ahead of time, one common front-end mistake might be to embed answers to questions within the component itself.
For example, the
ButtonSelect component would store the response within its own local state. In React, you would use
setState, or in Vue you would use the
If that were the case how would you handle conditional rendering? Reading data from sibling components is always awkward. How would you extract all the answers to the quiz when the user submits? What if more than one block needs to set/share the same data?
Aligning your programmatic entities with how customers think about your product is essential. Aligned entities make designing and reasoning about features easier for all parties involved.
By thinking at this level, you can reason about how things should be organized. Quizzes have sections, sections have steps, steps have content blocks, etc…
As you can see above, there are certain blocks that could be adapted to any situation, while more custom functionality should probably be a custom block (not flexible). Understanding these types of requirements upfront helped design the data and components.
A lot of the time, it’s the job of the back-end team to make decisions about the data structure, the API, and the endpoints. This is rather backward since the front-end team is the end consumer of the data. Data transforms on the client-side have lots of problems (larger package size, slower run time, possible inconsistencies between the web and mobile platforms, etc). A well-run team will actually have a dialogue between these two units. The front-end team would describe the type of data that would make writing the code the most convenient, while the back-end team pushes back on any concerns or implementation difficulties.
In order to improve the product, the code must also be flexible and adaptable. Product people are always analyzing and designing new features, creating new content, and thinking of new ways to target and communicate with customers. Unless your architecture is flexible where it needs to be, engineering will be a huge bottleneck.
Before starting this project, we had a problem with our old quiz. Our questionnaire was built with a fixed flow: pre-defined pages/views with fixed behavior. This made A/B Testing both difficult and unscalable because it required engineering to make every small edits for every test. What we needed was a tool that let our UX and non-technical people easily build and make edits.
Making a single copy change, adding a new conditional message, or adding a single custom tracking event without a builder would cost around a 30 minute to an hour per change. Why so long? Most of these changes would require a short meeting to describe the change to an engineer, the engineer needs to find the correct file, make the small change, then create a pull request. Then there’s QA, code review, code tests, and deployment.
The builder app changes that turnaround time to about one minute. Talk with people to figure out what tools will have the most impact!
Thinking about Flexibility
An internal CMS tool may not always be the correct solution to your problem. Flexibility means your code is easy to change and add to. This means good separation of concerns (you can make changes without worrying about it affecting other things), and adding options/switches you can flip to make changes. Strategies for flexibility will be tackled in a later article.
A common statistic that is thrown around is that developers spend ~9x more time reading code than writing it. In order to have a good developer experience, the quality of communication within your application is extremely important.
No one wants to spend time jumping from file to file trying to find out what they care about. This means that good organization is paramount. Good organization requires some amount of foresight. This means a week or two is spent creating a good architecture, and then creating a short 400–1000 word document to help communicate the architecture to the team. This effort at the start will save you and your team months of time in implementation.
Good Code Organization?
Have you ever worked on a project where all the files are organized by “type”? Views go in one folder, components go in another folder, actions go in another. Sounds fine right? Work on a Ruby on Rails app? Work in a React app? Notice how all apps more or less look the same even if they are doing vastly different things. What’s problem?
Organizing code by type is like writing a textbook and organizing all the knowledge like a dictionary or glossary. People read your book and understand the files in isolation, but they get pretty lost if they’re trying to see what your program does overall.
What makes more sense is to organize things based on goals/concerns. Goals determine what we have to keep in working memory and what the code is doing. It helps you find the specific code you’re looking for without getting buried in context switching.
Don’t be afraid of non-standard file structure
The root directory for our app was as follows:
Developer Convenience/Framework Specific Folders
- Assets — Folder for images, fonts, etc
- Blocks — Generic reusable components
- Compositions — Application-specific components
- Library Imports — Initialization scripts for different libraries
- Styles — The base style-sheets that includes all the variables, functions, and mixins.
- Builder App — The app for building quizzes
- Viewer App — The app for viewing the quiz
- Step Blocks — Components used to build steps within the quiz
You can think of each layer of the folder as one layer of concern. With this, each folder helps expose one layer of the program. At the top level, you are more concerned with how pieces fit together. As you go further into the file structure, you become more and more concerned with implementation.
The idea here is that your folder structure helps communicate how your program works.
When done correctly, the reader can understand how your program fits together. They don’t need to hunt through the file directory for files that are relevant to that level of abstraction (it’s all in the folder). It also means that it’s very clear where new files and folders should go relative to that layer, making contributions easier. It is also a natural place for a README to go!
The secondary benefit is that it naturally provides a more “plugin”/modular architecture. Don’t need that segment of the code anymore? You can just delete the folder and the correlating import. No need to fish around looking for other files that might be dead weight.
This is a short overview of some of the high-level considerations needed to design a good front end. We covered how to set design goals and some of the large problems you will need to solve.
- Consider all stakeholder
- Discover entities
- Communicate with your backend to describe the data you want
- Figure out requirements
- Think ahead on implementation and potential problems
- Decide what needs to be flexible
- Think about file structure and documentation
- Separation of concerns
- Component composition
- Converting goals and problems into architecture
- Testing tools and strategies
- Planning and executing refactoring
The next chapter will be on choosing good separation/grouping of concerns!
Thanks for reading! Please leave any questions and feedback or topics you want covered!