As the first engineering hire at Ergatta, Joey Marshall has seen a lot of changes at the fitness technology company over the last two years, in terms of team size and product features.
Because his main priority is building the software that runs on Ergatta’s rowing machines, it’s paramount that his work is able to handle substantial updates while providing customers with an exceptional user interface.
To pull it off, Marshall said it’s important that his team prioritize scalability when building products. Not only does it enable his team to iterate quickly, but it also prevents problems from popping up as Ergatta’s tech evolves.
“The type of scalability I'm most concerned with is the size and complexity of our app,” Marshall said. “As the app — and the team that builds it — grows larger, the more painful unaddressed performance issues can become. It becomes difficult to fix these issues later.”
While scalability is a goal for many engineers, achieving it requires dedicated processes and design choices that can fall to the wayside as companies expand their headcount. According to Marshall, that hasn’t happened on his team. Built In NYC sat down with him to learn how Ergatta’s simple design choices, targeted performance testing and emphasis on maintainability have kept its tech a delight for engineers and users alike.
Inside Ergatta’s tech stack
- Back-end: Go, PostgreSQL, several libraries
- Rowing machine tablet: Android libraries and Kotlin
- Custom game engine: C, OpenGL
Tell us a little about your tech stack and the tools your team uses.
Every tech choice has its tradeoffs. We heavily prioritized rapid development early on, which led us to go with Android as the primary way we build our UI. This choice was especially useful during our initial prototyping stage, as it was a platform we were most familiar with.
The downside to this choice has been that Android's UI libraries are not well-suited for some parts of our user interface’s design. Was the tradeoff one of the better ones we could have made at the time? Probably. But we’re always evaluating the best approach and open to new tools as we move forward.
One example of utilizing new tech is the internal tooling we’re using to build the next generation of games into our platform. We designed our custom game engine to be live-reloadable, both for the games and when making changes to the engine itself. Being able to see code changes automatically compile and take effect in less than a second, while also retaining game state, has made development much more enjoyable.
How does your team approach maintainability in your codebase? Why is maintainability important in the long run?
I like to think of maintainability as how easily a change will break something somewhere else. If I can look at a function or class and make a change with full confidence that there won’t be subtle side effects in other parts of the codebase, that is an easy change to make. On the other hand, if the code I’m looking at is interacting with state in a dozen other systems, seemingly innocuous changes can turn into difficult-to-find bugs.
My two favorite ways of managing maintainability are keeping surface area small, and statelessness.
Marshall explains surface area and statelessness
- Surface area refers to how many ways code touches other parts of the codebase, meaning the fewer points of content a component has, the harder it is to accidentally break it.
- Statelessness has a number of advantages, one being how much safer it makes points of contact within your codebase. Systems need to connect on some level and multiple connection points on a single component become safer if you know one connection point won’t affect the behavior of other connection points.
Pure statelessness is very difficult, if not impossible, when using an ecosystem so heavily influenced by object-oriented programming as Android. But that doesn’t mean we can’t borrow heavily from it when possible. Using Kotlin helps with this, as the language provides a number of functional-style programming features.
What does performance testing look like at Ergatta?
There are many facets to a high-performance application. Two I find important for a graphical application are the following: How well does your app handle unexpected input? Does it maintain a steady framerate?
It’s easy to accidentally write code that assumes a perfect environment. Network access is a very common point of failure. If the app doesn’t account for even a single network request failing, or inadvertently relies on request-response times or ordering, the UI may break in ways that are frustrating to users. These types of bugs can be difficult to reproduce or even detect. To combat this, we use a tool called toxiproxy that will artificially create network issues during development. This helps us catch areas of our code that falsely assume a perfect network environment.
It’s easy to accidentally write code that assumes a perfect environment.”
Frame rate, or how fast the display updates, is an often overlooked performance characteristic. Or more specifically, when a frame is missed is often overlooked. Most displays update 60 times per second, which gives the application roughly 16 milliseconds to process user input, calculate animations, update the UI and send draw commands to the GPU — if you want to maintain a steady frame rate.
When you take longer than 16ms, updating the display with a new frame is missed. When a frame is missed, or “dropped,” the application has to wait another 16ms before changes in the UI can be displayed to the screen. The effect is very subtle, and most users are not able to place their finger on it, but these stutters have a huge impact on how quality an application feels overall.
How are you encouraged to plan with scalability in mind?
Despite the hopes and dreams of product managers everywhere, software is not the type of problem you can throw more bodies at and have development progress with the same linearity. I’ve found productivity per-engineer begins to drop dramatically after the 3rd or 4th programmer working on a project.
When growing our team, we isolate new-feature implementation from the rest of the codebase as much as possible. It allows engineers to avoid coordinating with others on the team as much. If changes you’re making are potentially breaking features everyone else is working on, that’s where you run into efficiency problems. The easier a codebase is to maintain, the easier it is to scale the number of people working on it at a time.
I usually measure how well I do by bug count. If code I wrote doesn’t have bugs as a result of changes elsewhere in the codebase, and changes to the component I wrote doesn’t break other people’s code, I consider that a success.
How do simple code design choices contribute to rapid iteration?
Software iteration can be basically described as three steps:
- Implement an idea.
- Realize the idea’s flaws.
- Throw away (or mostly throw away) the result of step two and go back to step one.
We do this because much of the time, an idea’s problems aren’t apparent until you’ve built it. The key to this necessary process is what you do in the first step. To make this process quick and painless, one of the most important code design decisions is to write your code to be as easy to remove as possible. The easier it is to completely delete your work, the easier it is to repeat step two.
One of the best ways to make code easy to remove is to avoid over-engineering. It’s tempting to think, “Oh, this feature will be useful later, I'll make an abstraction.” The likelihood of that abstraction being useful is much lower than you’d think. The beautiful thing about easily removable code is that it’s also easy to refactor if a more engineered approach ends up being warranted.