Along with a number of “monolith to microservice” transitions, I’ve been a party to many “functional team to cross-functional team” transitions. I think there are some patterns and rules of thumbs that are widely applicable.
The place these transitions fall down is where the definition of “cross functional” is too wide. You can’t throw a UI programmer in with a data engineer and expect them to be competent at each other’s jobs. My experience is that the right definition of “cross functional” is “across an entire service,” with the expectation that all engineers should be working across the entire service.
This is one reason why I prefer larger services to smaller — it ultimately creates teams that are able to more fluidly solve problems, and creates fewer interdependencies between teams while still delivering user-facing value. And perhaps counterintuitively, there’s also less of a question of where to put stuff with fewer, larger services. We like to believe each microservice has very clearly defined responsibilities but that’s almost never the case.
It’s also worth noting that teams should be able to work in other codebases whenever needed. The “Web App Feature Team” needs to be able to work in the “Native App” service/codebase to build a feature it will need, with help from a “Native App” team or individual. In fact this is often a standard mode of operation if you have a monolithic frontend — teams without frontend expertise will need to work in that codebase to implement at least basic versions of their features, and vice versa.
So that leaves us with some number of services, and at least one team per service. The other big problem area I have seen with cross-functional teams is that teams begin to balkanize rather than work together. This is a natural tendency as humans. Our team is the in-group, our service is home. So we need to actively counteract it.
There are two ways to do so. First, reduce the friction of development in every service through some amount of standardization, but especially high-quality code. Programmers need to feel confident they can make a change safely. Like, in every repo should be able to make a change and write a test, and run
make test to make sure it works, and use
make run to start the process to test it out manually. This naturally requires clean, tested code. If you don’t have that… well, you need to achieve it, because nothing else you do is going to overcome dirty code and lack of tests.
Second and more interesting, there needs to be a sufficiently sized group of people working across services, solving problems outside of standard feature work. My experience (and thesis) is you should take one engineer from each service and have them serve on a rotation on this team, for maybe a month at a time. You may also want some full-time staff in this role to help enforce a consistent development strategy and set of values. This team should carry the pager on a rotation for all services.
Working on MMORPGs, we called this the “live team” and they were always some of the most creative and sharp individuals, who did not get the accolades of normal feature teams but made more of an impact. It’s also a role often filled by engineering managers, but I think a formal arrangement works better because it provides some real accountability and visibility.
This team owns build and release systems, developer and language tooling, performance diagnosis and tuning, and stuff like that. They ensure improvements can be made across all codebases in a timely manner, and knowledge disseminates across the entire engineering org. I’ve seen this group spearhead common library development (pulling code out of application codebases and cleaning up/formalizing the concepts), and make codebase-wide code changes to standardize logging, for example.
That’s my experience/thesis anyway. It may be obvious but bears repeating also that moving to a cross-functional model is difficult and there is more to it than just changing structure; though I think this is a good structure to begin with as a model. And for heaven’s sake, we need some alternatives/iterations on the cargo-culted Spotify model.