Project structure and modular development best practices

project-structuresalesforcedxunlocked-packagevs-code

I want to stop using the usual force-app/main/default single folder project structure in order to better organize my projects and decouple dependancies.

While researching about this practice, I found different examples:

When going through the examples, I came up with the following questions:

  • What does the usual force-app folder aim to?

    • In the easy-spaces-lwc example, such folder is replaced by es-base-code, es-base-objects, es-base-styles and es-space-mgmt.
    • In the sfdx-falcon-template example, it seems there is even one more level of organization with the sfdx-source folder, and inside of there, force-app is replaced by my_ns_prefix, unpackaged and untracked. Is this assumption correct?
  • What does the usual main folder aim to?

    • In the easy-spaces-lwc example, within each respective top level folder, there is just the usual main/default folders.

    • In the sfdx-falcon-template example, within each respective top level folder, there is not only the usual main folder, but also one folder for each feature. Where maybe one feature can correspond to, for example, one epic, since I understand it as a set of coupled funcionalities, which, if wanted, could usually be delivered as a different 2GP or unlocked package. On the other hand, with the organization structure from the easy-spaces-lwc example, this will not really work, since each top level folder (es-base-code, es-base-objects, es-base-styles and es-space-mgmt) does not correspond to a feature, but just organizes metadata types, mainly for deployment purposes. Is this assumption correct?

    • If I assume my previous point about feature folders is correct, and could usually be delivered as 2GPs or unlocked packages, wouldn't I need to specify each feature folder at the same level of force-app or my_ns_prefix in order to be able to deliver them as such, as suggested in this blog post?

    • Also, assuming we want to deliver them ad different 2GPs or unlocked packages, wouldn't it be better to have them in different repositories instead, as suggested in this stack exchange question? So far, I would work in a way where the whole project itself would be what I would have in one single repository.

  • Is the default project really needed?

    • If so, does it need to be inside of a main folder?

    • If not, how does sfdx understand where the default location for newly retrieved/pulled metadata should be? Since in the sfdx-project.jsononly the top level folder is sepecified as default. For example: force-app, es-space-mgmt, sfdx-source/my_ns_prefix

      I ask this because in the sfdx-falcon-template example, within my_ns_prefix, feature folders directly contain the metadata folders suchs as classes, aura, layouts, and so on; but the main folder, is structured inside with the default folder, but also with domain, schema, service, test and utility, and each of them contain the metadata folders. So it seems that the main folder has even one more level of organization.

      • Which is the exact purpose of each folders in this type of project structure?

Based on the information I have so far, I would like to came up with an approach similar to the one suggested in the sfdx-falcon-template example, but instead of having feature folders at the same level of my_ns_prefix/main I would rather have them at the same level of my_ns_prefix to have the possibility of delivering them as separated unlocked packages, being all the project in one single repository. Something like this (please note that I am intentionally removing main/default from extra feature folders, since default there will be only one, and instead of having different modules at main level, there are at force-app level):

  sfdx-project-dir
  └─ sfdx-source
    └─ core-functionalities
       └─ main
          └─ default
             └─ objects
                └─ MyObject__c
                   ├─ fields
                   └─ MyObject__c.object-meta.xml
       └─ domain
          └─ classes
       └─ schema
          └─ customMetadata
          └─ customMetadataTypes
          └─ customObjects
          └─ customSettings
          └─ platformEvents
       └─ service
          └─ classes
       └─ utility
          └─ classes
    └─ extra-functionality-one
       └─ domain
          └─ classes
       └─ schema
          └─ customObjects
       └─ service
          └─ classes
    └─ extra-functionality-two
       └─ domain
          └─ classes
       └─ schema
          └─ customObjects
       └─ service
          └─ classes
    └─ unpackaged
    └─ untracked

Would you say this is an scalable project structure? What changes would you suggest to it?

Although I understand that there is not only one single way to properly organize your project, I would like to know the exact meaning and purpose of each folder to take my decissions on how to name and organize them, and I would also expect to get the opinion of people with experience in this kind of modular develoment. I have found various reputable sources about this but It would be great if I can consolidate them here, being up to date.

Best Answer

While researching about this practice, I found different examples:

Yes, these are two Salesforce-published examples, two different possible ways to organize metadata. Both of these, and other ways, are valid.

What does the usual force-app folder aim to?

It's the default directory when you create a new project. It's just meant to serve as an example, though a lot of people use it, either out of laziness (it's already there), or under the impression it's somehow magical. Easy Spaces disproves it, but not everyone seeks out the documentation, and just start off with force:project:create, which creates these directories for you.

In the easy-spaces-lwc example, such folder is replaced by es-base-code, es-base-objects, es-base-styles and es-space-mgmt.

This is great for separate, potentially unrelated packages, especially if you plan on building dependencies.

In the sfdx-falcon-template example, it seems there is even one more level of organization with the sfdx-source folder, and inside of there, force-app is replaced by my_ns_prefix, unpackaged and untracked. Is this assumption correct?

Yes, there's no rule that all projects must start in the project root. This makes it clear what's not package directories, and what is. Here, they have managed package code, unpackaged code for Package Version validation, and everything else that's not destined for use in a Package.

What does the usual main folder aim to?

Main is the default "feature" directory that everything gets dumped to when you force:source:retrieve or force:source:pull. If you don't have one, it will be created for you.

... this will not really work, since each top level folder (es-base-code, es-base-objects, es-base-styles and es-space-mgmt) does not correspond to a feature, but just organizes metadata types, mainly for deployment purposes. Is this assumption correct?

If you look at sfdx-project.json for Easy Spaces, you'll see that base-objects and base-styles stand alone, while base-code depends on objects, and mgmt depends on objects, base-code, and base-styles. These are packages with dependencies.

Note that ES shows off creating dependent packages, while Falcon serves as an example to ISVs that want to use Second Generation Managed Packages to build their AppExchange apps. Everything for one package needs to be in one directory, which is why it's organized by feature.

If I assume my previous point about feature folders is correct, and could usually be delivered as 2GPs or unlocked packages, wouldn't I need to specify each feature folder at the same level of force-app or my_ns_prefix in order to be able to deliver them as such, as suggested in this blog post?

No. You can have your folders organized almost any way you want, as long as you don't violate the "magic directories" rule. For example, you can't have a directory called classes that contains non-Apex code (e.g. classes/objects/Account/fields/Industry.field-meta.xml would be invalid, but classes/utils/maths/MathUtil.cls would be acceptable).

The only other rule is you can't nest path folders. This can cause intended side effects. Just don't do it. Each package must be in its own, unambiguous space.

Also, assuming we want to deliver them ad different 2GPs or unlocked packages, wouldn't it be better to have them in different repositories instead, as suggested in this stack exchange question? So far, I would work in a way where the whole project itself would be what I would have in one single repository.

Splitting in separate repositories only really makes sense for completely different apps where there's no shared metadata. Once you want to start doing dependencies, it's far easier to have a central repository that stores the sfdx-project.json file; most IDEs and so on work better with one repo than many.

That said, smaller repos have better performance, less time spent pushing/pulling/merging. The downside is that you might need to synchronize files across several repos if you do this. There's also a third option, which I believe is a happy medium, and just use git submodules.

With submodules, you can have a main repository to work on, but each submodule will be an additional repository that can be updated independently. This cuts down on clone/pull/push times, as you can only pull in metadata chunks that you need for a project, but still have a way to keep your main version numbers and package IDs in a central location.

Is the default project really needed?

default is where previously untracked components end up when you force:source:retrieve or force:source:pull, unless it already exists in a different feature folder. If you don't a default already, it will be created for you.

If so, does it need to be inside of a main folder?

Yes, main is the place where default lives. Again, if you don't have one, you'll get one, whether you like it or not. Metadata found in the package in another directory will be updated if it is found, but if not, it will end up in main/default.

So it seems that the main folder has even one more level of organization.

Yes, it does. This is how it should probably look after being massaged into submission. Again, sfdx doesn't know for sure, so, if the metadata doesn't exist, it'll create something like main/default/classes (e.g. for new Apex code). If you don't want this to happen, create the file locally, push, and then you can edit on the server.

Which is the exact purpose of each folders in this type of project structure?

The purpose is organization. In the Falcon example, there is core metadata used throughout some/all of the features, and the features themselves contain metadata related to a specific feature. This can make it easier to navigate if some care is taken in how it is organized. The names are intentionally generic, because it's a sample, but you could have features like 'credit-check', 'delivery-system', 'customer-service-enhancements', etc. Or not. It's really up to you/your team to decide how it should look. At my job, we generally organize by core functionality, such as a section for utilities, one for each major function in the app (e.g. admin, trigger framework, etc).

Would you say this is an scalable project structure? What changes would you suggest to it?

As long as you remember the "Create Locally, Push, Edit Remotely" rule of thumb, you should mostly be okay. You'll end up spending some time having to move files around manually, but for most projects, it seems workable. Of course, you might discover you need to make some changes later (such is the way with doing new things), but I think you're on the right track.

Ultimately, I don't believe there is a One True Design that must be followed for success. If you can look at the directories and easily understand what their purpose is, it's probably an acceptable design. Just remember that you can't violate the "magic directories" rules, and you'll be fine.