Working with larger clients as a technical leader means steering several distinct teams toward a similar goal. Communication and alignment are not easy but become possible when the right guardrails are put in place. In this article, I’ll make the case for software design patterns and explain how to get them adopted by your teams.
Design Patterns? You Lost Me.
Have you ever run into an issue that you just know someone has solved before? A situation where you stop yourself mid-function-declaration and say “What am I doing? Why reinvent the wheel here?” and then find that a Stack Overflow search quickly serves up the copy-pasta you ordered? In the world of software architecture, we call that copy-pasta a “design pattern”. Design patterns are architectural solutions to common problems. They’re solutions that have come from years of developers solving the same problem different ways and agreeing on a solution.
The trick here is to remember what each pattern is intended (and not intended) to be used for and to learn how to weigh its pros and cons. Implementing the wrong pattern in the wrong situation tends to make things complicated.
I Get The Gist, But Why Should I Care?
When you’re working on a large project in an agile environment, you’ll usually find yourself on one of many small teams working toward the same goal. Those several small teams are going to encounter similar problems, and they’ll need the same solutions. It should be easy for TeamA to sift through and review code from TeamB. When priorities shift, TeamB should even be able to send over resources to help TeamA. This can happen effectively when all teams have a joint understanding of the high-level concepts that are being used in the application. Here are a couple of questions that each team should be able to answer the same way:
- What path does your data take getting from your database to your user’s browser?
- How are messages passed inside of your application?
- How is your UI broken out and organized?
If these questions can be answered the same way by your small, agile teams, then you’re in good shape! However, if you’re like most others in this situation, that’s not the case. One way to get there is to establish a set of common solutions for your application to follow at a high level: you need to establish a set of design patterns.
Ok, I’m On Board. How Do I Start?
There are a lot of ways to start here. I’d like to guide you through one of our recent projects to illustrate some of the decisions made along the way. This project was a product listing application for an ecommerce site. It was a data-driven user interface powered by APIs that were out of our control, so we didn’t need to worry as much about that side of things. We broke our scope out into three sections: UI structure, data flow, and inter-component communication.
Step One: UI Structure
Our approach was driven by our user interface, so we started to think about how to break that up. Our design decompositions showed us many reusable pieces of our user interface, which led us towards using a central design system to house our many reusable components. This led us to our first design pattern decision: we would use the composite pattern to structure our page components. This pattern has become popular given the prevalence of frameworks such as React and Vue.
Step Two: Data Flow
Once we understood how our application would be structured, we needed to identify how data would flow from our database to our user interface. Because we had multiple potential data sources being aggregated together based on our current environment, we quickly made our second design pattern decision: we would have an intermediary between our UI and our data source(s) that would implement the strategy pattern and aggregate our data appropriately.
One caveat to this was it could cause data coming into our page to vary wildly based on our data sources. This led us to our third design pattern decision: the data contract between our page and our data layer should remain consistent through the implementation of the adapter pattern. For it to work, the new advertiser data within our strategy pattern example would have to conform to the existing data structure that our page was expecting from our social components. For our experiment to work correctly given our new constraints, we’d need to change our user interface to allow for either data set to be accepted.
Step Three: Inter-Component Communication
In a fairly complex application, you may find that your user’s interactions have widespread effects on your page. On a page where updating a parent only updates its child components, we may be able to rely on the built-in patterns that exist in frameworks like React or Vue. If we’re not using a larger framework, or need further abstraction, we will need to make a decision here. Many pattern choices exist, such as the observer pattern that describes the model and view relationship in many MVC architectures or the publish-subscribe/broker pattern that is commonly used in libraries like Redux or EventBus.
In our case, we wanted our functional components to be tightly coupled to the components they’ll be managing state for, and we also wanted them to directly orchestrate events that happen within their scope. This led us to make our fourth design decision: we’d implement the mediator pattern to orchestrate interactions between our components. This would ensure that our interactions were handled explicitly by our components, relieving any object ambiguity that comes with the other mentioned patterns.
If You Cross The Railroad Tracks, You’ve Gone Too Far!
Remember when Google Maps wasn’t a thing, and someone would give you directions like “Take a left when you see the CVS?” Maybe it was just my family, but here’s my shot at it with respect to design patterns: talking about a great solution is easy, but it’s more difficult when that great solution becomes a bit overbearing. Sometimes a team or technical leader discovers the idea of design patterns and has a sudden and unstoppable urge to establish a pattern around everything. But too much of anything can be a bad thing.
Establishing a set pattern to solve a non-existent problem can bring more overhead than benefits. Over-optimization is something we have to fight early on in projects. Earlier in my career, I worked on a project that was structured using a layered architecture pattern. Here’s the problem: that business planned to spin up a third-party team to handle the data structure in an API, which would encapsulate the business logic as well. Eventually, accepting a new property from our API meant changing the object in our data layer, our business logic layer, and our presentation layer while making no modifications in any of them. We were passing this object through two layers of code that had become obsolete because we implemented the wrong pattern due to our own lack of business understanding. Unfortunately, this sort of thing happens when we move quickly in our development cycles. As a technical leader, I hope you’ll recognize these situations and plan to pivot away from them effectively.
How Do I Sell It?
We’re not always building a new application from scratch. In fact, that’s pretty rare. Teams are often opinionated, and most of the time you’re coming from a codebase that has many decisions already made. Furthermore, the people giving the funding aren’t always willing to shell out more budget for feature-less refactors. So how do you start to rally around larger changes from the top down?
Step One: Get Your Boss On Board
As a technical leader, you may need to get buy-in from the business before putting work into your backlog. When speaking to less technical folks, make sure you’re selling the vision, not the process. The end vision involves teams building software that can:
- Accept future enhancements with a minimal development effort
- Be discussed by the team using a shared vernacular
- Allow members to be on-boarded more quickly, helping to mitigate the risk of churn
Step Two: Get Your Team On Board
This should never be a “my way or the highway” conversation. These are the folks that have been in this codebase for quite a while, and they’re often able to point out pitfalls. For a team to have real buy-in in a time of change, they need to have a say in what that change looks like. For these conversations, I’d recommend that you do your research, arrive at the table with some ideas, and guide the conversation towards a solution instead of prescribing one. Remember, you are wearing two hats during these conversations: you are a stakeholder responsible for feature development, required to keep the overall business objectives in mind, as well as a team leader responsible for contributing to and guiding the technical focus. Those two hats aren’t easy to wear at the same time, but when this is done effectively, great things happen.
Don’t Just Set It And Forget It!
Once you have some design patterns discussed, agreed upon, and implemented, don’t forget about them! Design patterns need to be talked about and re-assessed as your software grows and matures. New team members need to understand them as well as their intent when they’re on-boarded.
Whether you’re a new team looking to spin up an application, an existing team looking to place more structure around your current systems, or a single developer well-versed in design patterns, I hope this inspires you to keep your mind and lines of communication open when it comes to improving yourselves and your product.