Developer skills feel like a bottomless pit
Each year, the bar to get into (and stay) in software engineering seems to get higher and higher. While “vibe coding” attempts to challenge that notion, the list of things that we “should probably know on some level” never seems to get shorter. Below you’ll find what I feel are some of the most useful skills a developer will need to know for web programming.
I’ve only ever done web engineering, apologies to the gaming and desktop engineers out there for my bias.
Ground rules
I want to try to set some context on what I mean by “what is useful". This list is not exhaustive, but is my vision of what effective web developers should have a strong grasp of. I’m sure we all have some opinions on certain implementations, so I will try to keep it high-level.
Object-oriented principles
Let’s start with the obvious, a developer will need to know a high-level language in Dotnet. C# is the workhorse, but F#, VB and C++ are very much alive in their own niches. Rather than be terribly specific on a particular language, let’s look a little deeper into object-oriented concepts in Dotnet as every developer should be familiar with how OOP works.
Classes vs Structs
We write a lot of classes in our day-to-day, but often times we forget we’re really creating a new Type
when we do so. Classes and structs allow us to create new types that are either reference types or value types. Reference types hold data/functionality in a different part of memory than value types. Reference types are “referred to” by a pointer to heap memory whereas value types are stored in the execution stack memory.
Reference types are often instantiated and we can essentially have as many as our system memory will allow. When we create a reference type, we choose the class
keyword for our new Type
.
Value types can be primitive things like integers, Booleans, but can also be things like DateTime
. When we create a value type, we choose the struct
keyword for our new Type
.
When passing in an argument for a class type, we are really only passing in the memory pointer under the hood. When we pass a struct type, we are passing in an equivalent value. This means we can have multiple copies of structs and they are all equal. The number 123
is the same as any other number 123
. The DateTime
representing 3/21/2022
is the same as any other DateTime
of that value.
Reference types are different b/c each instance of a class type is unique even if they contain the same properties as another instance. A developer should understand how equality works different between structs and classes.
Why this is useful: Create your types with purpose, deliberately architect your domain in a way that makes the best use of memory.
Abstractions
Without abstractions, we wouldn’t be able to have standard contracts between classes. If you’re not using abstractions, you’re not likely writing automated tests. Do yourself a favor and add an abstraction for your classes so you can keep your soldering iron on the shelf.
Why this is useful: Even if you don’t believe in automated tests (unit/integration/etc), being able to trade in/out a concrete implementation without rewiring your classes is critical to long-term maintainability.
Access Modifiers
Controlling access to the data from other areas of your application is critical. Far too often I’ve seen where fellow developers define objects as being mutable. By default data should be immutable from outside of your own code. Entire schools of thought are built around this principle alone.
Why this is useful: If you find yourself using public setters for everything, you have to realize you’re giving up custody of the data.
Async/Threading/Concurrency
Dotnet languages are very mature and have been around for decades. Ironically, with maturity comes encapsulation of concepts to make things easier for the developer. With that ease of use, understanding of how things work under the hood become perishable. While we are all grateful we aren’t coding in assembly language these days, there is a level of understanding about threads that ever developer should know. Make sure you understand what libraries are doing for you.
A good developer understands threading, concurrency and how the async Task Parallel Library works. Without this, a developer will never be able to get the full power of Dotnet tamed.
Why this is useful: Threads have largely been abstracted with TPL, you need to understand how you can simply run out of threads by waiting on calls to your database or making network/IO calls. You also need to keep async and parallel separate in your mind and when to leverage each.
Process Locking
In general, we want our apps to run as fast as possible. Often times we need to leverage parallelism but there are times when you need to make the “work” become serial by lining up one-at-a-time. To do this you’ll want to know some of the main ways to do it:
The
lock
keyword lets the runtime know that only one thread should be able to enter a block.Use the
SemaphoreSlim
class when needing to handle locks in async code.Use Redis or something similar when you need “out-of-process” locking. This is often called “distributed locking” to help ensure only one worker is processing the same work.
Why this is useful: You’ll wanna know how to keep code running concurrently to avoid duplicate work.
CIL/JIT
Dotnet developers need to know what happens when they hit “build” on their IDE. I’m a little surprised sometimes when I encounter a dev who isn’t quite sure what happens during this process. A developer should know, so let’s do a tiny dive. Aside from syntax checking, the build process compiles the C#/F#/VB into Common Intermediate Language (CIL) (aka Microsoft Intermediate Language (MSIL)). The cool thing is that this creates interoperability between the different Dotnet languages. You can depend on a DLL written in VB in a C# project!
If you were to peer inside of a DLL, you’d find intermediate language (IL) instead of binary machine code, we should be aware that this code is not yet executable by the CPU. To run our code, we need to have the just-in-time (JIT) compiler translate our DLL bytecode to something the local CPU can execute.
Why this is useful: It’s important to know what happens when you go from code, to build, to deploy. It’s quite a wonderful bit of magic that you should at least be familiar with.
DevOps
Taking a step back from the coding/building bits, a Dotnet developer needs to be capable of deploying their own code. Without this ability, your efforts to write an application will go in vain. Deployment of code as well as scripting infrastructure has spun into a dedicated career field at many organizations. You may be able to avoid having to learn some of this, but you will be limiting your employment options if you neglect this skill for too long. Something you’ll wanna consider learning are listed below:
Basic SFTP - Adding this is a the most basic way to get code on to a remote server. Not the sexiest option, but it gets the job done.
Build automation - You’ll do well if you’re able to script your builds based on source control. Continuous Integration/Continuous Delivery (CI/CD) is the point you eventually want to get to, until then prepare to spend some time repeating yourself on each iteration.
Cloud hosting - If you’ve never used Azure, AWS or any other cloud provider — you’re missing out. Many employers will be in the cloud, don’t get caught flatfooted.
Docker - “It works on my machine, so they shipped their machine”. Docker is a way to define a machine image and deploy it as a baseline.
Why this is useful: Apps that are not deployed are useless, get it out there.
.Net FW vs Modern Dotnet
Let’s face it, many companies are still using legacy .Net FW. Skills for that stack are still in demand but you really need to be working with modern Dotnet too. From config files, to Dependency Injection, to running on Linux. The future of Dotnet dev’s is on the modern stack. Even if your employer isn’t using it, do yourself a favor and start a side-project with it.
Why this is useful: There’s never been a better time to use modern Dotnet, don’t get left behind.
Background Tasks\Scheduling
Applications typically respond to input/events (web requests, event delivery, etc). However there’s going to be a fair amount of times where you’ll need to have the app provide its own trigger. HangFire is a popular app for such things that can be configured to run on-demand or on a cron schedule.
Why this is useful: Periodic things need triggers, this can be done a variety of ways, just have at least one way in your toolbox.
Dependency Injection (DI)
DI is an add-on for .Net FW, but for modern Dotnet; it’s a first-class citizen. You will need to know how to use it regardless if you love or hate DI — it’s here to stay. For apps, you’ll need to know the three lifetimes:
Transient - You get a new instance each time you ask.
Scoped - You will get the same instance per scope.
Singleton - You will get the same instance per app.
Why this is useful: DI seems like magic until you start digging deep into it. You’ll need to know which lifetime to use and how scopes (and nested scopes) operate or you’ll be in for some surprises.
ORM/SQL/NoSQL
Object-relational mappers (like Entity Framework) are ubiquitous in Dotnet. Know how to use them, know the traps; but more importantly — know how to use SQL without an ORM.
If you’ve never learned about NoSQL, now is the time. NoSQL DB’s like Redis offer advantages that a relational DB can’t. It’s not a matter of which is better, it’s a matter of which is better for the problem at-hand.
Why this is useful: ORM’s are a tool, they can easily be misused. Unfortunately with these sorts of tools, negative reinforcement tends to be how we all learn (the hard way).
Event-Driven Programming
Most of all programming is event-driven yet we don’t necessarily acknowledge it. Your mental model of external input should really be in the form of “it’s all just events”. For confirmation, all of the following are events:
Web Requests - Handled by a web controller for short process handling. These events keep a connection open with the publisher until a response is given.
Keyboard/mouse input - Captured by the operating system, passed into your application for handling.
Queue messaging - Handled by a message handler for long-running processes. These messages don’t reply to the originating publisher.
Server side events - Events pushed to a client from the server.
Why this is useful: Get into the habit that events are everywhere and not all events are created/handled equally.
Domain Modelling
One thing that will surely guarantee driving me nuts is when I inherit a project is the lack of modelling. In web, I see a tendency for developers to try to over-DRY (don’t repeat yourself) to the point where the DB-row becomes the domain model.
Properly separating the persistence, from the domain and also from the transport — is a critical skill. The trap developers find themselves in is conflating the three instead of recognizing the separation of concerns.
Why this is useful: You aren’t building a ball of mud, you’ll get one fast if you don’t recognize the need for fences even if it means repeating yourself.
Dynamic Dispatch
Since the “work” ultimate starts with an event handler, things like controllers can easily get cluttered and do too much. A useful way to keep responsibilities segregated is to use dynamic dispatch, or rather a way to route “work” to “handlers”. A super popular Dotnet dynamic dispatch library is MediatR.
Why this is useful: It’s all too easy to load up things into classes that do too much. By using dynamic dispatch, we can avoid hard-coding providers of “the work” by defining handlers of the work instead.
Architecture Tradeoffs
As you architect your apps, you’ll need to be aware of some common tradeoffs:
Sync vs Async - Using all sync threads can lead to thread starvation, using async introduces overhead but will free up your threads instead of waiting on work outside your app to complete. IO-bound work typically is async whilst CPU-bound work is sync.
Strong vs Eventual Consistency - In the real world, some things are the source of truth, some things are replicas. In order to live in a world where data eventually catches up to the source, we may need to accept the idea that some data stores may be out-of-sync for a period of time.
Orchestration vs Choreography - Orchestration uses a central coordinator while choreography might be a chain of linked events. Knowing what’s best in a situation will pay spades.
Why this is useful: If you’ve never pondered these three architectural tradeoffs, you might not realize there are times where one is better than the other.
Testing
This isn’t an argument for or against some sort of testing dogma. However you need to be able to have code that is testable and of course write some sort of tests. Pick NUnit, XUnit or MS Test library — they all do the same things. If you’re shipping bugs a lot, add more tests; if you’re defects are super low — you might be adding too many. Find a good balance.
If you’re writing testable code, then you should be able to write unit and integration tests at a minimum. Get familiar with Moq, it’s one of the most used test library companion.
Why this is useful: Planes might crash, money might not transfer properly, unicorns may die. Make sure your code behaves as intended.
Security
Hackers gonna hack, every developer should know and understand the basic OWASP common attack vectors. Don’t pretend to understand them, perform some of them on your app — be the hacker for your app, then defend against it.
Why this is useful: A fool and his money will soon be parted if you skimp.
Source control
I’ve noticed over the years that developers who can leverage Git are usually much more intelligent from their peers. There are some basic Git tutorials online, if you’re new to it or only doing the basics; learn a bit more to stand out. You should be comfortable with some of the command line and have a reliable way to visualize commits. I personally like Git Extensions, but there’s many to choose from.
Why this is useful: This is a required skill for modern developers. Don’t slack on this one, master this tool.
Polyglot
If you get thru your whole career only knowing one language, that would be remarkable. Most shops need multiple languages for various things. I personally push back on the concept of “one language to rule them all”. Examples of this can be found with Java historically attempting to be the end-all-be-all language (remember web applets?) and more recently with Blazor attempting to weed out JavaScript from our lives. I personally love both C# and JavaScript and I find that knowing multiple languages make you a much better person because you start to look at your favorite languages with more insight than you did before.
Why this is useful: Staying in one language makes a developer intellectually siloed. Learning how other languages handle the same problems as your first “native” language only makes you a better developer.
Additional Tools
Tools are a force multiplier, but only if you know how to wield them. I’m personally a JetBrains Rider fan, but Microsoft’s Visual Studio is also great. Some of the most useful tools I’m currently using:
JetBrains Rider/Microsoft Visual Studio - Primary writing/debugging
Microsoft Visual Studio Code - Vue/JS dev
JetBrains ReSharper/GitHub Copilot - Who doesn’t like some sort of help and assistance?
JetBrains DataGrip - My go-to DB Editor
While my list is fairly short of tooling, it’s important that quantity of tooling is not as good as quality of the tooling. You can really have too many choices and overpay and under-utilize them.
Why this is useful: There’s only so many hours in a day, make each minute count.
Engineering is hard
The above isn’t even an exhaustive list that engineers are asked to have a grasp. Engineering is hard and don’t forget to acknowledge you cannot be an expert at everything. The best thing you can do is put yourself in situations where you are not the smartest person in the room. There is no person in the world that cannot learn from someone else regardless of skill levels. Ask lots of questions, be prepared to realize you will be wrong — a lot. Surround yourself with successful people and build your team with varied skills, lean on each other when a problem comes along.
Be passionate, happy coding!