As described in the FAQ describing how modules install things, there are several different declarative registration mechanisms:
If you are implementing some API from another module, that module should tell you what to do. If it tells you something should be in the default lookup, that means to use the META-INF/services mechanism.
If you are defining an API in your module, and other modules will implement it and provide their own classes (think of the Navigator component in the IDE - different modules register handlers that can display navigation tools for various file types), manifest-based registration is out of the question, since there are no hooks to add new types of manifest section; and if you can possibly avoid it, you don't want modules to do anything programmatic, since that doesn't scale. So we're left with META-INF/services or the System Filesystem. Here are the questions to ask yourself:
Q: Is this just a simple interface or abstract class people should implement, and I just have to find them all and use them?
A: If so, use META-INF/services
Q: Will all of the objects other modules will register be used at the same time, or are there subsets for specific contexts?
A: This pertains to performance and scalability - wherever possible, you want to avoid actually instantiating the objects other modules install, and delay instantiating them until they really need to be used. Opening module jars and classloading are both expensive operations that slow things down.
If there will potentially be a large number of subclasses of your interface, try to find a way to divide them into context-appropriate categories and use folders in the system filesystem to partition contexts. For example, if you define the interface Eater, and modules will implement eaters of various foods, you probably should create folders for different general kinds of food. That way, you don't have to load a bunch of classes and create the OliveEater, BreadEater, EggEater and SausageEater just to ask them if they can eat the Pizza you came across.
Q: Is there information needed about how each object is to be used which could be provided declaratively, without instantiating the object?
A: If so, use the system filesystem, and either let modules declare file attributes thatprovide additional information, or let modules put their objects in subfolders that have contextual meaning (the same way the folder Loaders/text/x-java uses the folder path to indicate the data type objects pertain to).
Q: How many modules will actually implement my interface? How many objects from other modules will I typically be dealing with? Do I need to instantiate all of them just to display a list in the UI?
A: Displaying things in the UI is a particularly important case: You do not want to have to load a bunch of classes from a module just to show an icon and a display name for something - this pertains to anything you're going to display in the Options dialog, on Menus, or various other views.
You can solve this by using .instance files and asking that modules implementing your API use the file attributes defined for .instance files to declare the icon and display name. Then your code can just get a FileObject for each registered instance, call DataObject.find(theFileObject).getNodeDelegate().getIcon() or getDisplayName() to get the icon or display name of the object without ever having to create the object until it needs to do real work.
If you run the risk of having to instantiate all the objects in a folder just do trivial tests such as finding out how many objects of one class are in a given folder (which may contain objects of other classes), consider recommending .settings files instead of .instance files in your folders.
PENDING: Content okay, but should be split into more individual items