13

The wonderful capability to create 2GP Managed Extension Packages on top of 1GP Managed Base Packages is great luck for AppExchange partners that have built complicated and monolithic apps over the last years.

As described in depth in Vivek Chawla's epic answer partners can create multiple layers or modules of 2GP Managed or even Unlocked packages and slowly migrate code from the 1GP to 2GP.

My question gains to collect real-world experience and best practices for doing such migrations: How did you do it? What worked? What not? What do you recommend?

More specific questions where I would like to learn are:

  • What would you leave in the Base package? Data model? Triggers?
  • What can and should be moved into Extension Packages technically?
  • Which mechanisms work for sharing code between layers/modules?
  • Can a shared Namespace be used with an existing 1GP?
  • Where to put infrastructure code or platform services (Logging, Eventing,...)?
  • Do you use Dependency Injections like Force-DI or your own CMDT-driven approach for dynamic invocation?
  • How do technically move logic over? And do you use toggles to decide if to use the old implementation in the 1GP monolith or the 2GP module.
3
  • 1
    From an ISV perspective, I would say that breaking up a 1GP first requires 1GP to 2GP migration (pilot now) but also package bundles. Without the latter, installation is too complex and fiddly.
    – Phil W
    May 10 at 7:14
  • 1
    To clarify further, without the 1GP to 2GP migration, it is not possible to use the same namespace and thus you do not have backwards compatibility.
    – Phil W
    May 10 at 7:45
  • 2
    On my previous comment, this applies because you cannot install a 1GP and a 2GP in the same org if they share a namespace.
    – Phil W
    May 10 at 9:07

1 Answer 1

15
+100

This is a great topic. Many Salesforce Partners are exploring ways they can leverage 2GP today, even as they wait for 1GP=>2GP migration which is currently available only as a developer preview (registered partners only).

In true "safe harbor" spirit, let's look at how an ISV or OEM partner might use 2GP to reimagine an existing, monolithic, in-market 1GP as an MPSN (multi-package, same-namespace) suite of 2GP extension packages with the existing 1GP at its core.

Robert Sösemann's questions are an ideal way of exploring this topic, so I'll address each of them, below.

Can a shared Namespace be used with an existing 1GP?

No, it can not. A namespace collision occurs anytime you install a managed 2GP into an org where a managed 1GP of the same namespace is already installed. If you’re going to extend a 1GP with a 2GP, you must use a different namespace.

What can (and should) be moved into extension packages?

There’s no simple answer to this, but I can provide a framework for making good choices.

Step One: Find out which component types can be removed from 1GP and which ones can’t. Use the guide to Components Available in Managed Packages to find the answer and keep it in mind as you follow the next steps.

Step Two: Identify a compelling business reason, based on a 2GP-specific feature, for moving a component out of the 1GP monolith and into a 2GP extension. For example, logic and UX components are great candidates because 2GP’s model of flexible ancestry enables increased productivity and agility when building new product features.

Step Three: Consider the subscriber’s experience. For example, “moving” 30 LWCs results in your subscriber seeing 60 LWCs in their org because the original LWCs can’t be removed from your 1GP. On the other hand, Custom Objects and Fields can be removed from your 1GP but the disruption this would cause your subscribers is massive and should be avoided at all costs.

With the above in mind, here’s my generalized take on what should, and should not, move out of the 1GP monolith.

Should be Moved

  • Apex Classes (be careful with triggers)
  • Lightning components and pages (within reason)
  • Visualforce pages and components (within reason)

Should NOT be Moved

  • Objects and Fields (don’t torture your subscribers)
  • Custom labels and translations (don’t pollute the subscriber org)

What would you leave in the Base package? Data model? Triggers?

The previous answer speaks to this but I’ll expand here on the topics of data model and triggers.

I would always leave the data model in the 1GP base package because of the potentially massive (and painful!) impact on subscribers. The exceptions would be any protected Custom Settings and/or Metadata required by anything that got moved into a 2GP extension.

Be prepared for how “sticky” custom objects can be. For example, an action override that uses an Aura component results in that component and its dependencies getting “stuck” in the base package.

Triggers are more complicated. Ideally, I’d like to take all of my Apex out of the 1GP and put it into my 2GPs. The feasibility of this depends on the trigger framework being used and the capabilities/bandwidth of the development and support teams, though. I'll talk more about this when I answer the next question.

How do you technically move logic over? Do you use toggles to decide to use the old implementation in the 1GP monolith or the 2GP module?

“Logic” in Salesforce comes in two flavors: Apex, and Flows. My answer will focus on Apex, specifically global Apex and Apex Triggers.

Moving Global Apex

global Apex is required anytime classes, methods, and properties are called outside of a package’s namespace. Using global Apex in your 1GP monolith usually means you want to expose that logic...

  • To subscriber-owned Apex or Flows
  • To external systems via Apex webservice methods
  • To extension packages (1GP or 2GP)

Because global Apex can’t be deleted and the signatures of global methods and properties can’t be modified, “moving” logic from the 1GP monolith to a 2GP extension boils down to...

  1. Recreating that logic in an extension package
  2. Installing that extension package in subscriber orgs
  3. Directing subscribers to update dependencies to point to the new logic

It’s critically important to ask yourself three questions before recreating logic in an extension package.

  • Do I want to continue exposing this logic to subscribers?
  • Do I want to expose this logic to third-party developers?
  • Do I need to expose this logic to 2GPs that my organization publishes in other namespaces?

If you answer “NO” to all of the above, you should tighten down access from global to public in your extension packages. Once in 2GP, you can use the @namespaceAccessible annotation to expose public Apex classes (concrete and abstract), methods, interfaces, and properties to other 2GPs in the same namespace.

Helping subscribers prepare for, then adapt to the new structure of your solution is the most important part of this process. Keep your logic working in both 1GP and 2GP for a couple of releases, then begin a process of deprecating the 1GP APIs. How quickly you do this depends entirely on what your customers are willing (or able) to do and how fast (or slow) they can do it.

Moving Apex Triggers

This is where things get complicated. Complete, working implementations of your triggers will be required to run side-by-side for a couple of releases. To make this work, you must implement logic in both your 1GP and 2GP-based triggers to prevent them from running at the same time.

If you’re already using a trigger framework that supports activation toggles, you’re ahead of the game. If not, be prepared to invest time and effort in adopting one before moving forward. It’s also OK to ask yourself if it’s worth the trouble to move the trigger logic. Don’t move something out of your 1GP monolith unless there’s a compelling business advantage that’s specific to 2GP.

Which layer/module should I put infrastructure code or platform services (e.g. Logging, Eventing) in?

Believe it or not, in my opinion the best thing about 2GP is not multiple packages in a single namespace. It’s flexible ancestry. That’s why the most important thing to consider when designing the layers/modules of a 2GP app is this:

Never use multiple packages unless you have a compelling business reason to do so.

With that in mind, when designing an MPSN (multi-package, same-namespace) solution, start with a packaging strategy that balances flexibility with simplicity.

Foundation (Bottom Layer)

  • Custom schema (objects, settings, metadata, etc.)
  • Core UX (default pages/layouts, action overrides, etc.)
  • Custom Labels and Translations
  • Third-party Apex libraries
  • Trigger framework that supports MDT-driven dependency injection
  • Triggers (standard and custom objects)
  • Platform Event definitions

Abstraction (Middle Layer)

  • Data classes
  • Service classes
  • something related to dependency injection? here or in Foundation?

Application (Top Layer)

  • Trigger handler logic (feature-specific)
  • Feature UX
  • APIs

Should you use dependency injections like Force-DI or build your own CMDT-driven library for dynamic invocation?

I’m not familiar enough with Force-DI to endorse it but I do know that the contributors are all rockstars in the world of Enterprise Development on the Salesforce Platform.

I do think that you should use a trigger framework that provides a mechanism for trigger handler logic living outside of the Foundation package to be “registered” with the framework as a post-install step. Such a framework would allow the subscriber admin to toggle packaged trigger logic on and off, regardless of which package the logic actually lives in.

I’d love to hear suggestions and real-world stories from the community about this. Reach out to me in the Managed Packages group in the Partner Community to share your perspective.

Which mechanisms work for sharing code between layers/modules?

The workhorse of cross-package work in 2GP is the @namespaceAccessible annotation. As noted above, it’s how you expose public Apex classes (concrete and abstract), methods, interfaces, and properties to other 2GPs in the same namespace.

Outside of that, things like Custom Metadata Types, Custom Labels, Platform Event definitions, and more should be usable between the different layers of your app. I’m working on a definitive list for this, but the best advice is to never assume something will work as you expect it to until you run a test that proves it.

Closing Thoughts

2GP offers many advantages over 1GP. Every net-new package going to market should be built using 2GP. Partners building a 1GP app that hasn't passed Security Review should stop what they're doing and rebuild with 2GP right now. Trust me, there will never, ever be a better time to make that switch.

Partners with in-market 1GP apps can easily add new functionality with 2GP extension packages. Recently, though, I've seen an increase in the number of partners who want to do more than just extend their app with 2GP. Instead, they are reimagining their core solution by moving existing functionality out of a 1GP monolith into an MPSN (multi-package, same-namespace) suite of 2GP extensions.

Revamping a monolithic, in-market 1GP is possible but it's not for the faint of heart. The end result can absolutely be worth the trouble, but success depends heavily on your capabilities, the amount of runway you have, and the profile of your customers. Do your homework before jumping in, but don't pass up the chance to use 2GP in this way if you feel it's within your reach.

6
  • 2
    With my Packaging1 goggles on, I still dig shared namespace and @namespaceaccessible. We're working in a small team, never head issues with linear releases - would you still say Ancestry is what we should be excited about? May 16 at 8:01
  • 1
    For me, non-linear ancestry only becomes interesting when the package only contains automations and not metadata that is directly consumable on the subscriber org (such as custom objects, fields, flow templates etc.). I think package decomposition into function-based modules makes a lot of sense (though likely a real refactoring challenge) supported by core modules/frameworks. A small OCD point, both the IsTest and NamespaceAccessible annotations should start with initial caps to be consistent with the naming of the other annotations (IC2 supports this consistent capitalization).
    – Phil W
    May 16 at 10:14
  • 1
    In terms of the "closing thoughts" comment I'd like to qualify that; adopting 2GP to encapsulate existing elements of a 1GP is a heavy lift, but use of 2GPs as extension packages is not. Of course, with the latter, you don't benefit from the @NamespaceAccessible functionality since you have to use a separate namespace.
    – Phil W
    May 16 at 10:19
  • 1
    @PhilW - Huge thanks for your comment about my "closing thoughts". I was writing that after midnight and it didn't quite reflect my POV. I've updated it now. @Christian - Don't get me wrong, I love @NamespaceAccessible (capital "N" for my friend Phil!) but even small teams can benefit from flexible Ancestry. Not to "avoid issues" but to experiment in entirely new ways that linear versioning just doesn't allow. I do agree that package decompostion can be awesome, but until we get a viable solution for package bundles I'm taking a cautious approach to building out too many packages. May 16 at 15:04
  • 1
    @VivekM.Chawla, shouldn't "Data classes" and "schema" (objects) be in the same layer/package ? That would seem logical, otherwise adding fields to existing objects and data classes would involve potentially updating multiple packages.
    – altius_rup
    May 16 at 15:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.