1. 28

For many projects, it’s desirable to be able to have a mix of skill/experience levels on a team such that everyone is able to usefully contribute. But some code bases seem to be much better-suited to that than others.

I’m curious what common factors people have noticed in projects that result in junior engineers being productive, and on the flipside, what factors make for a project where only senior people can get anything useful done. Have you seen a project successfully go from one of these styles to the other?

  1. 29

    I’ll go a little against the grain by suggesting that the easiest codebases for junior devs (or anybody without a lot of context) have a lot of duplication and little abstraction.

    My experience is basically: juniors do well to trace down the flow of execution of a thing. Whether it’s a frame of Quake3 (best internship I’ve had) or a web request, nothing beats being able to write a list on a piece of paper of “okay, user does this, then this gets called, then this gets called, then this happens.” This is made easier with compiled languages and less metaprogramming, but that’s a side note.

    So, you want to make it easy for them to trace logic.

    Similarly, you want them to be able to make changes without having to fight the entire codebase and maybe break something–because any codebase that’s been responding rapidly to changing needs is probably going to have less than ideal test coverage (because reasons), it’s important that you can make changes that don’t break other parts of the system.

    So, you want to have a codebase that prefers duplication in places instead of having a few powerful load-bearing abstractions that if a junior breaks will destroy everything–or even worse, actively prevent their work, causing them to make improvisations around the abstraction that grow like so many vines among marble columns.

    Taken together, I think that we could do a lot worse than giving our juniors applications with lots of clear not-really-DRY with clear “cable runs” where any new functionality should go or exisiting functionality should exist. This probably looks like obvious file structure, obvious control flow, and simple idioms.

    Of course, mentorship and senior dev accessibility are more important than any technical solutions.

    1. 6

      juniors do well to trace down the flow of execution of a thing. (…) nothing beats being able to write a list on a piece of paper of “okay, user does this, then this gets called, then this gets called, then this happens.” This is made easier with compiled languages and less metaprogramming, but that’s a side note.

      This is true for essentially everybody, not only junior engineers. A good rule of thumb for everything is to be as explicit as possible, avoiding any kind of “abstraction”, even if this leads to some duplication.

      1. 2

        I would add have a single caveat to this: It’s a good benefit to be able to virtualize the world outside your program (so that you don’t have to spin up databases/message queue/file system trees) for testing purposes.

        But yeah, the more concrete you can make your solution, the better.

        1. 1

          I’m guessing you’re not a .Net/Java dev, that would be considered heresy!

          1. 2

            Hehehe, they are to programmers what Bourbakists are to mathematicians. In other words, they are part of the “french school”: you should write the most general construction possible that solves your problem. The opposite way is the “russian school”, where you write the least general thing that solves your problem.

        2. 2

          Similarly, you want them to be able to make changes without having to fight the entire codebase and maybe break something–because any codebase that’s been responding rapidly to changing needs is probably going to have less than ideal test coverage (because reasons), it’s important that you can make changes that don’t break other parts of the system.

          I think this is important. To really learn what works and doesn’t work, what’s maintainable, and what’s actually extensible, you need to try stuff. I believe this is true at all levels, but especially true for junior engineers. I would say a good bit of code for a junior engineer to start on would be:

          1. Sufficiently isolated that they can try things out without breaking everything or accidentally having bad designs ossified. (i.e. Don’t get them to work on a core library shared with other projects)
          2. Not so isolated that they aren’t learning about the existing code and picking up ideas from known good code. (e.g. Don’t get them to write a completely new piece of code)
          3. In a situation where the junior dev can be given some responsibility for maintaining their code, so that they can experience the consequences of their decisions (good as well as bad!), preferably with plenty of opportunities to try to fix or refactor it if there are issues.
        3. 20

          Documentation is key. If not though comments or a wiki, then at least through having libraries and frameworks up to date. If they get old enough, sometimes their maintainers tend to leave out the docs for those ancient versions.

          Source: am Jr. dev and struggling through a codebase that doesn’t follow this.

          1. 5

            First off I just want to say, don’t see yourself as a “junior”. I find this term degrading to hard working people who are just as bright and brilliant as anyone else on the team. There is only familiar, and unfamiliar. And over time you will shift toward the former.

            Secondly: sure, you lack legacy knowledge your co-workers created themselves. In the absence of documentation, your most powerful weapons are grep, and find (alternatively any programs that do similar functions).

            Using just these two programs you can very, very quickly create a mental graph of the dependencies between files and code. I’ve navigated +1000 file code bases I’ve never touched and made changes no problem using just those tools.

            As you do this, you create your own documentation, and eventually you start to get ideas how to make dealing with the system better. Then you integrate your documentation into the legacy systems and people love you for it. :)

            1. 5

              Why do you see the term as degrading? I’ve never seen it as that, merely a marker of experience in the workplace.

            2. 2

              Once you start making changes and are able to subdivide parts of the code mentally, it gets a lot easier. I’ll also add another thing - documentation is often wrong. After reading lots of documentation and seeing inaccuracies, I mostly only trust the code now. Documentation can give you a high level, but just like asking someone how something works, you often get “how it should work” instead of how it actually does.

              1. 1

                I feel for this. We have a junior on our team that is trying to navigate a heavily customized Magento 2 codebase and at times it feels like they’re being thrown for a loop. I try my best to document and leave behind teachable things, but I also have the responsibility and expectation of building forward - so there’s only so much I can actually do.

                I wish you luck, don’t give up!

              2. 16

                Helpful colleagues and time.

                But, also, the ability to examine & run bits in isolation, ideally, the desktop workstation.

                1. 10

                  One factor that I think is true of mixed-skill-team projects I’ve worked on is that the code is structured to limit the minimum required scope of changes. That is, it is possible to make a useful change that only touches a small corner of the code base, and, more critically (and harder), it’s quick and easy to identify which corner of the code base a given change will need to touch.

                  Modularity and encapsulation are necessary but not sufficient to get there, I think; it is possible to have highly modular code where a large percentage of changes end up needing to touch more than one module, which is likely a sign the module boundaries were decided purely on technical grounds rather than taking into account the boundaries of the concepts in the business logic. To some extent, this relates to “bounded contexts” in domain-driven design, but I think bounded contexts are usually wider than I’m talking about here.

                  1. 9

                    Good, maintained, didactic documentation, and a domain expert on the code base who can field questions. The code itself is almost an afterthought, a liability — the asset is the domain knowledge, and documentation is the way to incept that asset into new people.

                    1. 9

                      Everyone else has already cited the basics, I’d add: A body of people who actually know the codebase in question well enough to answer questions and who actually have cycles to do so.

                      I’ve watched way too many juniors left swinging in the wind because their ‘mentors’ have zero actual cycles for them.

                      Sad and ultimately disappointing for mentor and mentee in equal measure. Avoid.

                      1. 8

                        The codebase should already be “good” or time needs to be spent to describe all of the things that are bad about it. Otherwise, the jr. developer will pick up bad habits from the patterns in the codebase.

                        1. 7

                          Clearly defined layers of abstraction. Good documentation.

                          Tests. I really appreciate a well tested codebase. It’s really nice when you have confidence that the existing tests are good.

                          Consistent patterns in the code. Some codebases have A LOT of experimenting. I think that if a team wants to adopt a new pattern, they should put in the effort to make sure that the old way of doing things is replaced. This isn’t always easy, especially with deadlines and aggressive estimations of work. Keeping up to date documentation of “best practices” is a good first step.

                          1. 2

                            I agree with the tests part. They are kinda all the documentation I need — a lot of code has complicated setup requirements and tests give you minimal working examples and show you how you need to call the methods. I don’t know if you can reasonably expect documentation in real-life software projects but at least if there are tests it’s easier to get familiar and build confidence.

                            Another thing is maybe a build system or process that is not too difficult? Like if you can’t manage to get stuff to build because there’s a million dependencies that all need to be built in the right order, that can also be a problem.

                          2. 5

                            Honestly, the first thing that came to my mind as I read your question: The codebase should have as much code as possible which would produce good code if a junior dev imitated it. Meaning to say: It should exemplify all the values, priorities and qualities that the most senior developers in the team want in their codebase.

                            However, that said, a good codebase alone isn’t enough to have junior engineers produce good code. It’s more important that the team itself is good. (For some definition of “good team” – left as exercise for the reader)

                            1. 4

                              Good code organization is pretty important. I’ve noticed junior devs getting lost in large codebases that are unorganized.

                              1. 4

                                what factors make for a project where only senior people can get anything useful done

                                Great distance between edits and seeing what’s going on, and also in general the inability to inspect anything going on with the system:

                                • debuggers don’t work or are too slow (lack of inspection)
                                • no logging system (did my code even do anything?)
                                • no built-in debug overlays or modes (lack of inspection)
                                • no safe test environments to run experiments (how do I ensure I don’t break production?)
                                • not being able to run the entire system locally (competition of resources with senior devs)
                                • very long feedback loops such as with extremely long compilation times (what was I working on? reduces ability to experiment)
                                • excessive tie-ins with systems acting at a distance (not being able to see all the pieces.)
                                1. 3

                                  A lot of people are talking what the codebase should be for juniors to be productive, so I’ll talk on what I think the codebase should be for juniors to improve. From my experience, having a codebase in a “transfer” state from legacy to new code, with clearly defined boundaries, is important for setting the examples of good code vs bad code. Rewriting bad code into good code helps building good code standards, and understanding what code does. For me personally this was what helped me to progress in software engineering.

                                  1. 3

                                    From my experience, a couple of things:

                                    • easy dev environment setup
                                    • easy deployment
                                    • docs
                                    • solid project plan
                                    • functioning code base with parts they can mimic
                                    • example tests

                                    Basically, for new developers, the less you need to ask the to do outside of the codebase, the better.

                                    1. 3

                                      The #1 thing IMO is the ability to run the application locally, since this gives new engineers (whether new to the project or new in general) the ability to try everything except some external integrations without any risk.

                                      1. 2

                                        I have recently been working on a project as senior sw dev and architect, and mentored some junior colleagues.

                                        I think some processes and culture is important, and devoting personal support for the newcomer until getting acquainted with the tech at hand.

                                        What we found helping was:

                                        • not overly abstracting things
                                        • having some small complexity small priority tasks “stashed away” for introductory work.
                                        • having a good CI in place
                                        • having lots of simple tests
                                        • having mandatory review, and strict but friendly review culture
                                        • having job rotation between teams as it highlights parts harder to grasp, and helps creating a knowledge map to introduce newcomers (not necessary juniors)
                                        • having a one-on-one mentoring devoted to the junior while getting started with an unknown territory. This meant I was devoting about half of my time to an intern in the first two weeks, and another guy being the lead of a “neighboring” product did also like me for 2 more weeks.
                                        • having docs would have been nice, but when I was mentoring I did draw/sketch a lot about the architecture and interactions of components, neighboring and further ones, to let the Junior colleagues understand the possible impacts and failure modes the component they need to touch would encounter.
                                        • do pair programming in TDD together with the interns/juniors, where sometimes you write some tests, and the junior has to make it pass, or vice versa. It is a fun way to introduce some new concepts, and share knowledge, almost like if playing.

                                        My experience was that with this amount of attention devoted to the juniors I have worked with they were up at full speed after the first month, when given work they were already introduced to. This gave them confidence, and in one more month they could take on tasks from other, slightly unknown parts of the system as well. They were also confident that they could ask, and get answers.

                                        I loved some moments, when for example one of them did complete a task, with tests and all, and on the review after reading a page of code I asked: would you do this part in LINQ too to compare the two solutions? We did it together, and it was solved in 5 lines of code. First the kid felt a bit bad about the first (totally correct) solution he wrote alone, because he worked a lot on that (shuffling data to lists and dictionaries and looping over them in very oldschool ways), and felt that work was wasted, which we talked over why it was not wasted work, we have just found a more maintainable alternative, which makes sense to consider from a business point of view. Even a year later sometimes the guy would come and ask if we could crack some complex data mangling together in a more functional-programming way when he got stuck, but then he brought some harder ones we had great fun solving on pair programming.

                                        I really loved mentoring, was the best part of the works I ever had.

                                        1. 2

                                          I have dissent from documentation here. Documentation is great, and definitely turns a good codebase into a great codebase, but junior engineers often don’t know when to look it up, or what to look up. It’s the act of finding that junior engineers need help with. As a junior engineer, I still sought for help from people first. That was always the critical path: getting someone to point the way. As a senior engineer, I now am on the reverse side: I spend most of my time pointing the way. I infrequently see cases where documentation would have helped someone get there. It absolutely happens, and is way more important in libraries compared to applications, but it’s secondary.

                                          The things that immediately helped me when I was junior were things that were passive benefits. Examples include

                                          • Static tooling: having a linter, a prettier, and a typechecker running at all times makes it so much easier to figure out how the codebase works without actually running the codebase. Being able to test out a variable rename and seeing all the places it’s used – amazing. (Imagine having to, for example, rename a template variable.)

                                          • First-class editors: Everybody loves Emacs and Vim, and the configuration adventures you have to go on, but junior engineers already struggle to remember arcane shell incantations and CLI tools. Something like Visual Studio on Windows, which is still one of the best development experiences you can get, goes a long way toward onboarding a junior engineer. Combined with static tooling, now you have something is substantially smoothing out the onboarding process.

                                          These are two of the reasons TypeScript is seeing more adoption, I think. If I want to write a product that’s going to last 5+ years, I want a codebase that I can throw lots of junior engineers on because the tooling and the editor will support them. For frontend code, I don’t think anything currently beats TS and VS Code. Everybody always talks about type systems when it comes to picking frameworks and tech stacks, but it’s money, tooling, and IDEs that matter first. imo

                                          1. 2

                                            I honestly don’t know. I suspect these would help: lots of tests, short stack traces, short class names, same-but-different kind of tasks.

                                            I think writing code is not the only goal, maybe not even #1: sometimes a whole day of capturing profiles, reproducing bugs, & writing down test cases is super productive. To give them the feeling they need to push code to prod soon or GTFO is not entirely helpful (and I suspect a lot of juniors feel that, if they’re not told outright).

                                            But I wonder: what cars are good for apprentice/novice mechanics to work on? How does Habitat for Humanity use non-construction workers to build houses? What kinds of systems make progress thanks mostly to non-experts?

                                            EDIT: as pnathan reminds me, if you don’t have a standard dev environment, you are entering a world of pain

                                            1. 2

                                              Just from the start, if there’s a reasonable way to get the application to run and poke it helps a lot. Get started and don’t be disheartened by having to fight the first week until it finally runs on your machine and you have to update 10 wiki pages with “the 2020 instructions, as observed by me”.

                                              The opposite is also true. Junior developers are not experienced code auditing experts who can look at a code base without being able to run it and instantly grasp it. Probably most people aren’t, I’m still not sure how security audits exactly work ;)

                                              1. 1

                                                Apart from the things already mentioned in the comments (documentation, wiki, up to date libraries), which I agree with, my personal experience for getting to know a codebase is through doing tasks that “cut” through several layers of abstraction in the code base but don’t require any groundbreaking changes in the higher levels of abstraction; just an extension to already existing functionality in the lower levels of absctraction. This way, they have a glimpse of what is underneath a function call to another library but they don’t have to worry about the implications of the changes they could introduce. They also wont get too bogged down with debugging code that requires a lot of implicit knowledge.

                                                1. 1

                                                  Good Design is all about how little you need to read and understand before your can make a useful code change.

                                                  No other criteria matters as much, nor drives design choices as much.

                                                  It doesn’t matter whether you’re a junior, or a grey beard slaving in the same multimegaline code base for decades.

                                                  Why? Because nobody can keep a megaline of code in their head.

                                                  Tattoo the following list on your arm… http://sweng.the-davies.net/Home/rustys-api-design-manifesto

                                                  If you’re operating down at a miserable level three, put an URL to where the current documentation lives as a comment next to the relevant code.

                                                  Debug tricks and tips must live with the code.

                                                  Every time a junior must get up and ask a senior to get unstuck, because the senior couldn’t be arsed to document their secret squirrel… you have put a huge roadblock and morale killer right there.