How to avoid wrecking your architecture while chasing new features: a survivor’s guide

Oleg Plotnikov
Miro Engineering
Published in
7 min readMay 5, 2021

I joined Miro on day one as a frontend engineer. Today I manage core teams working on the canvases’ inner core and real-time collaboration within them (we’ll touch on what core teams are later on in the article).

Miro is growing fast! In 2020 alone, we:

  • Passed the 10 million registered users milestone
  • Handled a sevenfold increase in activity load (compared to the beginning of the year)
  • Doubled in team size (engineers, product managers, designers)

Feels good! But there’s a catch.

In my nine years at Miro, I have witnessed more than once how increased team size could harm development speed. New features require a growing number of workarounds and hacks due to the current architecture’s limitations and lack of time for refactoring. It becomes hard to decide who exactly is responsible for refactoring the code. Initiatives on architecture improvements are often abandoned halfway because team focus has shifted somewhere else. Mounting code complexity makes it harder for new developers to join the project and stalls their onboarding.

Let’s turn back the clock and see if there are ways of doing things differently to save our future selves from this series of unfortunate events.

Take one: save 20% of the time for refactoring

Let’s go to the time before things start to collapse under the weight of technical debt. Several dev teams are working on various new features. Each team is fully responsible for their feature, and they can change any code to implement it. The team’s current focus and backlog are handled by a product manager who has their own KPIs, depending on the metrics achieved.

Of course, this creates the “quality vs. speed” trade-off.

It’s the product manager’s “move fast and break things” mindset versus the engineers’ “let’s do this right”. On one hand, you’ve got to test new ideas, dismiss those that don’t work, and ship the ones that do add value. On the other, simpler architecture and cleaner code would help avoid so many problems in the future.

It’s not that the manager doesn’t understand the value of quality architecture, or the team doesn’t want to add features.

The real problem is that some changes in the architecture require the full team’s attention and focus for several months, which means no new features get shipped — an impossible scenario for a rapidly growing product.

An obvious disadvantage of this approach is the lack of system improvements that might take weeks or even months to implement. It also requires spotless development processes to free up at least 20% of the dev’s time for refactoring. Most important of all is the team’s own motivation to ship quality code and to not compromise.

In a smaller team, where each developer knows for sure it’s not the last chance they have to interact with the code they ship, motivation for quality should not be a problem. Yet, as the team gets bigger, the temptation to commit code that “just works” grows, since there’s a high chance that it will be somebody else’s job to deal with the consequences of a poor architectural decision later.

Take two: each team has ownership over their piece of the codebase

The idea is simple: split the codebase into components, and assign each of them to a dedicated team. Again, each team has total control over the features they ship, and they can add any new code that might be needed for a particular feature to work. If they need to change the code of a component that belongs to another team, the owner’s approval is required.

What does “being the owner” really mean here:

  • Review pull requests and other changes
  • Make decisions on major changes together and with the approval of the other teams
  • Consult and collaborate with the other teams
  • Meet company-wide code quality standards (tests and documentation included)
  • Fix bugs on production (goes without saying)

How does it work in real life?

A dedicated file in a repo holds the map of which files and repos belong to which team. When a pull request is created, all relevant participants that might be affected are automatically added as reviewers — meaning no code is changed without the team that owns it not noticing.

However, assigning responsibilities over the parts of a large system is almost impossible to pull off without help from upper management.

Also, while doing that we discovered large chunks of code that did not fit the “for each team, a feature” pattern in the long term.

The core teams I’ve mentioned in the introduction were created to deal with these fundamental parts of the system.

Take three: dedicated core teams for architecture and system-wide tasks

First, feature tasks and architecture tasks don’t get mixed up in a single team’s backlog. Core teams are fully dedicated to advancing product architecture. Their job is to lay down a solid foundation that the other teams can use to deliver new functionality, and to upgrade the existing. Their backlogs are managed not by product people, but by tech leads or systems architects, instead.

Here are some examples of how a core team task could look like:

  • Simplify a data domain model and its API representation.
  • Create an inner framework for the feature teams.
  • Isolate application layers.
  • Improve server performance and fault tolerance.
  • Perform major architecture refactoring that would enable new features development, previously unavailable.

We need to make a distinction between core and infrastructure teams. The latter are responsible for the environment, release launches, and horizontal services fault tolerance used in production (for example, databases). Core teams work with business logic, and they can’t make any long-term decisions without product management.

Great, now we have core teams! Is that enough, or is there anything that could go wrong?

Stuff can, and will, go wrong.

For instance, a core team could be turned into another feature team, because it’s more important during the product’s rapid growth. The core team could easily get overwhelmed with fixing other teams’ bugs and workarounds. They could be working on features or components no one’s planning to use anymore. Even worse, they could be building an architecture so disjointed from reality, that it would not contribute to any real KPIs.

There are many ways to drift. How do we stay focused?

Picking the right priorities is everything. It’s easy to get lost when the product gets bigger — there’s not just one major problem to solve; instead, lots of places need fixing, while feature teams keep generating new requests for core functionality.

The key is keeping long-term strategic focus, instead of getting tangled in local tweaks. This requires sitting down with the business team to define how the product is going to develop in terms of technology in the next couple of years (at least).

Take four: defining technological strategy

The main goal here is to provide a top-down view of the product roadmap for the core teams. The name itself is not important: Technology Strategy, Architectural Vision, Painted Picture, Master Plan — pick whichever you fancy the most. The important part is putting ideas from your heads on paper, where everyone can see them, and agreeing on what to do next.

The strategy should consider three sources of change:

  • Business logic: a grander vision for product development, strategy, and major feature releases that might get blocked.
  • Performance and fault tolerance: planned load growth; key metrics that drive service optimization.
  • Backlog for current blockers that stall development here and now, development convenience. Code quality according to the industry best practices.

Our first draft was conceived after several days of work together with our most experienced developers. As a result, after heated brainstorming and lots of coffee, we had agreed on:

  • System components and application layers — how we split the code.
  • Responsibilities between the teams — who’s managing which component.
  • Target data model — objects in the system and how they’re related.
  • To-do list for major changes in the core components that would boost or simplify the development and unlock new product features.

The next step was validating the strategy with technical and product management. We gathered feedback, and adjusted accordingly.

Then we spread the strategy across all teams, so they could refer to the target architecture when building new functionality and make decisions that bring us closer to it.

Having an artifact (a simple document would suffice) helps: it makes communication more transparent, and it acts as a reference for everyone. Teams use it as a source of truth when considering how a particular architectural decision would correspond to the overall plan.

Then comes the hard part of bringing your vision to life. Plan, implement, correct, repeat, and don’t forget to keep communication alive among all the stakeholders.

Final cut

Let’s walk through the action points that can help you stay flexible and deliver new features fast, without screwing up your architecture big time, given the product and the teams are rapidly growing:

  • Plan time for your teams to work on technical debt.
  • Agree on policies and code ownership, and adjust your development process accordingly.
  • Define long-term product strategy, business logic requirements, and whether your current architecture can sustain them. If it can’t, and if the changes can’t be implemented with the current resources, create dedicated core teams to pull together developers who know the inner workings of your product well. These teams bring architecture toward the target.
  • Establish a target architectural vision with the business team, put it on paper, and sync the product and development roadmaps accordingly.
  • Work hard toward the established goals, and don’t forget to communicate major changes across teams.

For us, it’s just the beginning of a journey — we’re six months into the “core teams” model. Thanks to the established strategy, communicating with product managers has become easier. Especially when it comes to agreeing on priorities: which features should be done now, and which should wait until the architecture undergoes some improvements.

The younger the project, the simpler it is to introduce such practice. Yet, there is no silver bullet — try things that would work best for your team, and remember: the good old “let’s do this the right way” attitude can work magic.

Join our team!

Would you like to be an Engineer at Miro? Check out opportunities to join the Engineering team.

--

--