Staring at a blank solution in your IDE can be a daunting experience. So too a blank whiteboard, waiting for a system to be sketched upon it. There are literally any number of ways you can structure that empty space. So where do you start?
I like to start by tackling the big question first: what's the shape of the application? By this I mean what type of application is it going to be. There are, I think, three main options here: monolith, client server, and microservices.
The first is simple, a single solution that will contain all your code. It'll be deployed as one unit, so scaling it means beefing up the hardware it's running on, or having a server farm with multiple instances of your solution running on it. The main benefit of the monolith is there is little to no orchestration of parts required. Everything lives in the monolith, the UI, business logic, infrastructure concerns... all of it. Conceptually this is as simple as it gets; so everyone can get up to speed quickly on it.
A step up from the monolith is to split your solution in two: a client and a server. This is the fit for most mobile apps, that have a native front end client for UI and a backend web api for everything else. This usually gives rise to a two team structure in your workplace - a front end team, and a back end team. The two can proceed at different paces from each other. In terms of orchestration, you need to define an api that the two pieces use to communicate. This is a non-trivial task to get right. You need to be aware of introducing breaking changes to your api. Versioning and documentation become concerns for you. You also have two separate things to deploy, but that can scale separately from one another. New types of client applications can be added if the need arises, reusing the existing backend.
Finally we have the on-trend microservices solution. This is exponentially harder to get right than a client server solution. There are many moving parts, many things to deploy and orchestrate. The front end client is likely to remain similar to the one from the client server solution, but the backend is markedly different. Instead of one service holding all the domain logic, it is split across many services. Which means you have to decide how you are going to split up your domain (not always easy), which bits needs to talk to which other bits (sometimes messy), and how they are going to do it (lots of options).
For small to medium sized projects the architecture concerns can dwarf the effort required for the actual application itself. What you get is the ultimate in scalability; deploy multiples of just the parts you need; beef up the hardware of just the parts that need it. Conceptually there can be a lot going on in a microservices solution, so getting people up to speed on it can take time. Some people will just plain struggle with it.
Once you have a shape, the next thing to address is how the innards of your pieces will look. Layering is what you need here. There are near endless ways you could do this, but in practice the same layers will usually crop up. Layers are a conceptual dividing up of your solution piece, backed up by one or more physical implementations. In C# land, a layer would usually map to a project within a solution; in Python world a package. Depending on your development environment, you may have few or many ways of enforcing your layering in the codebase.
Not all of your solution pieces need adopt the same layering - in a client server solution for example the two pieces would likely adopt very different layering. Microservices can in theory all differ from each other in terms of internal layering, but in practice adopting one approach aids comprehensibility of your codebase. That said, lets look at a common layering taken from the Domain Driven Design (DDD) world.
As you can see three of the layers go by more than one name, but they imply the same thing. So what is it that each layer does?
An application needs to present itself to the outside world. To this end some form of user interface is required. The code for it all lives in this layer, whether the interface is a website, desktop application, web api, or command line interface. Your solution may require more than one type of presentation. To use C# as an example, you might have a console application project and a website project that are both part of your conceptual presentation layer.
The presentation layer is concerned only with user input. So once a request from the user has been validated, it should hand over to something in the application layer.
The main entry point into your domain code. The application layer is responsible for orchestrating the components needed to service a user request. It can talk to the infrastructure layer to do things like load and save data from a database, or call an external api. It can talk to the domain model layer to run pieces of business logic. There are many ways to structure an application layer, but the concern is always the same.
Every non-trivial application has infrastructure concerns. These are things like accessing databases and external apis, writing to the filesystem, talking over the network to another process. The list is long. The details of how this is done are of no concern to your other layers. Indeed these things are liable to change over time, so need encapsulating in their own layer. Just like the presentation layer, you might have many projects/packages that make up the layer.
This is the high level stuff. It gets you drawing those first few boxes and arrows on the whiteboard, adding the first projects or packages to your IDE solution. How you structure each layer is a topic both deep and wide. We'll look at some of the common approaches in a future article.