Moving towards Object Oriented Programming in Salesforce
A beautiful image of "Object Oriented Programming"(?) by ChatGPT

Moving towards Object Oriented Programming in Salesforce


1. Intro

If you're reading this article, you might be wondering what I mean by "moving" towards the use of object-oriented programming. Aren't we already doing that? Don't we use classes? Apex is an object-oriented language, right? Aren't there already thousands of courses talking about the same thing?

The reality is quite different. Using an object-oriented language does not guarantee that we are really employing the object-oriented paradigm. It's not enough to simply call structures classes, methods, or attributes, nor to inherit from one class to another just to "reuse code."

Here, perhaps a bit philosophically, I want to remind us why we call it "Object-Oriented." The object-oriented paradigm is our attempt to abstract, recognize, and embody qualities and behaviors in objects or entities that, in many cases, we identify in reality or even abstractly, like numbers (but they are concepts, after all).

My motivation in writing this blog is to offer a different perspective, a critique, and hopefully to bring new ideas to software development in Salesforce. I hope that some of the ideas presented here will help you solve problems and manage your repository within Salesforce. I want to emphasize that this is not a magic solution, it's just another approach that tries to implement ideas of clean code, division of concerns, and focus them on addressing the inherent issues with using Salesforce. Much of what I'll share I know because I have applied what others have written; this is simply a Salesforce-directed interpretation. Among the authors I highlight are Mark Richards, Yegor Bugayenko, and Simon Brown.

The adoption of these principles, when applied correctly, results in more human, readable, higher quality code that can be systematically implemented (and yes, if that's what you want to hear, it also means less money spent on maintenance).


2. Object Oriented Programming

If you are a Salesforce developer, you might have come across class names that, instead of describing what they are, describe what they do. You might wonder, isn't it okay to name a class by what it does? Well, actually no!

When you name a class Account, you know what an account can do; you can assign contacts to the Account, you can send an email to the primary contact notifying them of something, you can calculate the revenue you have made or even the number of contracts to be closed until it moves up a tier. Thus, you name a class by what it is and it's the methods that contain the logic and describe what they do in that class. In the previous cases of Account, we are able to distinguish those behaviors that we would see as methods.

Classes that detail what they do can be identified because instead of class names being just nouns, these names also include verbs or consist of several nouns put together. Think for a moment about a class named CalculateOpportunityDayForecast. Do we have a real-world concept called CalculateOpportunityDayForecast? Many of these classes are actually trying to explain what is done with the objects; these objects are there, and we can identify them without a problem, we use them all the time. In the case of CalculateOpportunityDayForecast, if we break it down, we would realize that we actually have an Opportunity object, and this object allows us to obtain and calculate relevant information about a business opportunity and then, with that information, we can interact with another object, Forecast, which could help us generate a report.

Note! We will see some examples of classes that might break the naming rule, and that's because these classes represent connection points like batch classes, schedulable classes, Controllers (AuraEnabled), etc. Ideally, these should not contain logic but should function only as a presentation layer either to be queued or to be consumed from an LWC component. All these classes consume the logic found within the objects or, if required, a service that groups the behavior of several objects. For the examples that follow, note that for objects there will always be nouns, but for connection points, we opt for names like "controller", "service", etc.

This is an example of the thinking you can use to start identifying and finding which objects will interact and how they will interact. Questions that can help us include: Is this a concept, a noun? Am I naming this class by what it is or by what it does? Is there something in the real world with this name?

Let's see the following example of two codes that do the same thing! But they do it in very different ways. Both solutions will take a group of classes that need to be deactivated and will notify the contacts associated with those accounts about the change.

Procedural Style

Observe the following code! The truth is that although it is not wrong, it works! It exemplifies some of the problems with the procedural style.

First, although this is not exclusive to the procedural style, you'll notice that all the logic is contained within one method. While this is not unique to this paradigm, there is a tendency for developers to perpetuate it because, in many cases, it is customary or seems simpler to think only about the steps to follow and not about the structure.

Second, even if I divided it into smaller methods, I don't have units that allow me to distinguish which logic belongs to what thing. I can read it and make an effort to identify these units. If I see a for-loop of accounts, then some business process related to these accounts is happening. The code should speak to us, but the objects that are involved in the code or the logic units are sometimes so intertwined that they are really difficult to distinguish.

Go to repo!

Article content

Object Oriented Style

The following approach should speak for itself; the object-oriented structure allows us to have small portions of code that do one thing, and we can associate them with a class. In other words, we have a mapping of what an object does, its business logic, and we clearly identify it by its class.

Although we define more classes in this case and it may be the first thing you've noticed, we gain significant benefits in terms of flexibility and scalability.

First, we know that we will be informing Contacts of an Account, but not just informing contacts of one account, but of multiple Accounts.

For the following code, observe the two units that were extracted from the procedural paradigm. It was abstracted that:

  • An Account can be deactivated.
  • An Account can have relatedContacts.
  • An Account can be notified of some status change in the account.

Go to repo!

Article content

Although the previous example should work, the requirement asks us to perform this logic on a set of Accounts! Therefore, I can also abstract what would be a set of Accounts. Here we abstract that:

  • I can deactivate a set of Accounts.
  • I can load the Contacts for each of the Accounts.
  • I can reuse the Account to access this logic. See the deactivate method for the Accounts.

Article content
Article content

What flexibility do we gain? A lot! The code we have allows us to deactivate one or many accounts, choose whether or not to notify associated contacts, and handle these processes with considerably smaller and more specialized methods. Additionally, having the logic within our objects gives us the possibility to use this logic wherever it's needed.

Example using the code for a single Account

Article content

Example used in AuraEnabled

In this example, note that we take advantage of the fact that the method is not coupled with any SOQL query, so if we pass a list of accounts, as long as it has the appropriate fields, the "services" of our objects will continue to function.

This allows you to reuse the methods regardless of the specific query you need to execute. For example, you could select accounts based on different criteria without needing to alter the internal logic of the classes.

Article content

Unlike the procedural approach, where the code is tightly coupled to the specific output of the query and locked into a rigid flow, the object-oriented approach offers us more effective decoupling. Each class has a clearly defined responsibility, which simplifies testing and system maintenance.

In our example, the Account class deals exclusively with operations related to an individual account, while a class like Accounts handles operations on a set of accounts. This approach not only facilitates understanding and improves the quality of the code, but also minimizes the chances of error during future modifications.

In the previous examples, I have already used some of the techniques that I intend to cover and explain in more detail. If the previous example has convinced you, I invite you to hear a bit more about the paradigm. If not, I would like to hear your comments. Ultimately, I believe it is a good time to remember that this is not a silver bullet; what is best for your organization depends on many factors, and it is not necessary to do everything under this lens.


3. Intrinsic Problems to Salesforce?

If the previous example convinced you, you should know that there are certain limitations within the platform that restrict the use of object-oriented programming and indirectly lead you down a procedural path. Problems? Is it perhaps blasphemy? No, Salesforce is an exceptional platform; we have all sorts of tools: flows, we can create tables (objects), we have a programming language! In the end, what can't we achieve with code? Salesforce is huge, and sometimes as Salesforce customers, we forget the importance of managing the evolution of our implementations using these tools, and one of the most complex to manage is the code.

There are numerous blogs that discuss the SOLID principles, or Division of Concerns, and many of them focused on Salesforce, and I have nothing against them (many are really good), but we need to start talking about the problems with the platform and concrete ways in which we can apply these principles and deal with the inherent problems of developing within Salesforce.

The issues I will address are:

  1. The ORM and the Namespace, the use of the self attribute.
  2. Management in the codebase, use of structures.
  3. Collection classes, the solution for bulkified problems.
  4. Decorators

3.1. Problem or Advantage? Something very similar to an ORM...

In Salesforce, although it is not very clear, we have something very similar to an Object Relational Mapper (ORM), that is, a code component (which we do not see) but is transforming the database tables into runtime structures or "Salesforce Objects."

Put more simply, what you see in tables in Salesforce now appears in your Salesforce code as an Account or a Contact. This introduces a problem when generating code on the platform and it's not only related to the criticisms that have been previously levied against using ORMs:

One of the major issues introduced by the "Salesforce ORM" is that it removes the namespaces that would be appropriate or representative of certain objects (standard). Look at the following account of type Account; that Account is what Salesforce returns to us.

Article content

If I have an Account, you might think I can add more logic or extend that object, but this is not possible! That means the example we saw earlier for the Account would not work, because I don't have an Account class. In fact, if you try to create it, Salesforce will allow you to do it, but it will no longer distinguish between your class and what it originally thought was an Account (the object that contains the information from the query).

Article content

The second problem with this "Object" acct is that it is a dummy object. It doesn't contain much more logic than that needed to access the object's fields, and we also cannot extend the class! Therefore, even if we wanted to handle our logic in a Domain layer (by this I mean the division of responsibilities, or division of concerns), we couldn't do it.

Trailhead can help us a bit with this if you go to Domain Layer in Salesforce!

Article content

3.2. Results of the Problems?

If we cannot use an Account class, nor can we inherit from it, and we don't make an effort to solve this, then... WE SHOULD NOT USE object-oriented programming! Right? But of course, that is something that is already done in many cases, and the point of the blog is the opposite!

These problems corner us and lead us to use a procedural approach, which brings us to the problem of having an anemic domain model. The issue with the namespace can be resolved by using composition and defining a naming standard for our classes that represent standard objects.

Note two things in the following example: first, I named my class AccountClass, though it could actually be anything you decide—AccountWrapper, Account_c, AccountDomain, etc.

The second important thing about the example is the self attribute. I decided to call it self because of its similarity to "this"; in this case, self is the object that contains the information, the Account that Salesforce returns.

Article content

3.3. First Example: AccountContactCreation

Let's look at a first approach to a problem of creating contacts related to an Account. Observe the following class named AccountContactCreation (it says what it does, not what it is), which has a method called createContacts that takes two parameters: first, the contacts to assign, and second, the account to which they will be assigned.

Article content

This first approach to the problem might initially seem "fine" since, after all, we are "making it work." However, this approach brings other types of problems in the long run. Each time you need some new logic, you are adding an entire class. Moreover, when you need to utilize the properties of object-oriented programming that Apex provides, you won't be able to use it. What would it mean to extend a class like "AccountContactCreation"?

As the needs of your business continue to grow, you will have a problem if you add a class for every new process or logic you want. You might think that having a class for each functionality and naming the class by what it does would make things easier, but in reality, you could be duplicating large amounts of code without even realizing it.

3.4. Second Example: AccountClass

The second example uses the proposed self attribute. Starting a class this way may seem laborious initially, as we start with some boilerplate code, but let me explain.

Since I cannot extend the Account object/class from Salesforce, what I proceed to do is add an attribute called self, which is of type Account—the standard object we couldn't extend. In this way, we have a wrapper that will allow us to abstract and encapsulate the behavior of an Account; all relevant business logic occurs here.

When we need business logic for an Account that we have retrieved using a query or simply created, we can create a wrapper, pass the Account to its constructor, and execute all the business logic we need.

Observe the method assign(Contacts); notice that this method receives contacts as parameters, and it is understood that what it seeks is to assign these contacts to this account (this account!).

I am calling this new class "Account" + "Class" because I cannot simply name it "Account" due to what we discussed earlier about the namespace.

Note that I am not updating anything in the new method responsible for the contact assignments, allowing someone else to decide when to update or insert those!

Article content

Another advantage is the use of inheritance or polymorphism. For example, if in your case you need that for certain types of Accounts, the assignment of contacts goes through a validation, you can extend the AccountClass with a GoldenAccounts or PlatinumAccounts class. Of course, in Apex this will require defining the AccountClass as abstract and overriding the method.

Article content

3.5. Conclusions from Examples 1 and 2

So far, I have shown that we have wrapper objects that represent, not necessarily all, but the tables/objects that you consider and need. Using the procedural approach, your codebase will end up looking something like this:

Article content

With the current object-oriented approach, you could have fewer classes, your project structure can start to make more sense, and developers might be more aware of how to use and reuse the code that they already have available. This is simply because you are letting the code speak!

I took some of the classes from the previous example and converted them to what they would look like if they followed this principle. Note that what is under each class corresponds to the method. e.g., Quote has a method called newQuote.

Article content

You'll notice that now the logic can be identified for each entity, and you can begin to work in terms of "Objects" in the sense of object-oriented programming. This will also enhance developers' ability to identify the logic they are working with and provides information that can be mapped to the architecture of your Salesforce instance, similarly improving the architectural process.

3.6. More advantages of this approach: It eliminates problems with duplication.

Much of the code that gets duplicated is code that often seeks to filter by one field or another, or just to apply certain default values. In some cases, we might think we could reuse the same method from one of the two classes elsewhere, but this would introduce dependencies between classes that might initially be doing completely different things—and remember, low coupling... high cohesion. I know, it's something we should "ideally" do. Will there be insurmountable cases? Undoubtedly there will be, and we'll need to think!

Consider the following example where we have the same method, filterActiveAccounts, to filter "active" accounts in a batch class and another controller class (AuraEnabled).

Article content

Now look at the Controller; here we have the same function as in the batch class. This is a common form of duplication, often going unnoticed when writing complex methods. One piece of advice would be to review methods with a lot of nesting or many lines of code and identify patterns that perform the same actions. You will realize that in many cases, you can find logical units like the one we have here to filter active accounts.

Article content

We might think that if we have the same method within the batch class, we could simply use it now in the Controller, but this only introduces unnecessary dependency between the two classes, not to mention the changes in the batch class and how unnatural this approach feels. This is an attempt at failed code reuse.

Article content

So, where should I place the for-loop that filters the accounts(like a group of accounts)? It should be placed somewhere that is accessible to both classes. For this, classes that represent "collections" are useful!

4. Collection Classes

"Collection" classes follow the same design as we previously used for an object. The distinction is that they allow us to modify collections (lists) of said object.

This enables several things:

  1. Execute logic on batches of objects.
  2. Reuse logic from "singular" classes such as Account within Accounts.
  3. Use interfaces like Iterable and Iterator.
  4. Address problems that involve the excessive use of data structures such as lists or mappers within our code.

4.1. Solution for the code duplication

Let's analyze the following class:

  1. Notice that it now has a list of Accounts, which will be the collection we are "wrapping".
  2. Note that we receive these through the class constructor.
  3. Observe that I have two methods, here we have the activeAccounts which will serve to solve the problem of code duplication that we saw earlier.

Article content

The previous classes would now consume this method. Observe in the following case: the batch class now uses this Service that we have created to filter accounts in the execute method.

Article content

Now we can also use it in our Controller. Notice that we defined a layer that only had behavior; it had no DML, no SOQL, it was just behavior.

It is precisely these logical units of behavior that give us this flexibility. Another way to think about it is to ask yourself, "What can be done to an Account?" and answer with what you know you would do with a group of accounts.

  1. You could assign contacts to a group of accounts.
  2. You could make reports on their opportunities.
  3. You could perform a customer satisfaction report.

All of these are logical units that we can define within our class because they are transactions, processes, or business logic that we know are part of the Account or a group of accounts.

Note that we could have performed a filter in the query, but for demonstration purposes, it was made as simple as possible.

Article content

With this, we have achieved the following! A class with a well-established logical unit that other connection points (schedulable classes, AuraEnabled methods, REST endpoints, batchable classes, etc.) use! In the future, if we need to filter anywhere else, we know where to go to place or look for that logic.

Article content

4.2. Not everything is perfect! We will always have to think!

If you've been developing in Salesforce for a while, it won't seem strange to see data structures like the following in the code:

Article content

The result of having data structures like the ones mentioned is due to a problem on the platform that developers face, which is the inability to use SOQL or DML statements within for-loops due to limits.

One solution to this problem can be seen in a Trailhead batch that I highly recommend to developers, called Learn Unit of Work Principles. In this batch, the following code is shown for a test class, and I think just seeing the difference between both codes highlights the advantages of using the fflib (FinancialForce library).

Before using the library... (It's not my intention for you to read everything unless you want to, the change after using the library is obvious)

Article content

After using the library

Article content

The creation of objects, as in the previous examples, is considered one of the weak points of the object-oriented techniques we've seen. Even using collection classes, there are cases where the design can be complicated!

Primarily, if you create objects within our wrappers, you might be losing the flexibility that simply placing fields in the object returned by Salesforce or trying to encapsulate logic provided. Although the context of it being a test in the previous example worsens the situation, it is an indication that there should be an approach or solution for object creation that I am not aware of (yet). I guess we'll have to figure it out.

It's something I'm still discovering! The good news is that the UnitOfWork can be easily integrated by applying the techniques I've mentioned before. Still, there's always the possibility that you might not need or simply not want to use the UnitOfWork... it's always a valid possibility, as is the attempt not to be purist in object-oriented design! In this case, the best we can do is consider that the creation of objects might be associated with a factory for our test class. Thus, once these objects are created, we can retrieve them with queries and encapsulate the query results in our wrapper objects that contain the logic.

4.3 Iterable

One more example! This is a trick using the Iterable and Iterator classes. This "trick" allows us to iterate over each element that makes up a collection of objects, so we don't lose the flexibility that previously iterating over a simple list gave us. For example:

Article content

In the following class, observe the comments in the methods that are being implemented. Note the following:

  1. We are implementing the Iterable and Iterator classes. You can find these in the documentation under Custom Iterators. Note that we are implementing them at the same time instead of in separate classes.
  2. Since we are implementing both interfaces in a single class, note that the iterator method returns this. This is what makes the trick work to iterate over the class.

Article content

Example of usage

Imagine a case where we need a script and we need to use some logic from the Accounts class, maybe we don't want to do something very complex and simply execute an AnonymousApex. This could be a use case.

Article content
Note that the use of scripts often ends up being procedural, which is fine! I never said we have to use object-oriented for everything.

4.4. A Practical Example in Commerce Cloud

Now I'd like to discuss an example from Commerce Cloud, since it's something I've been learning about lately. Additionally, I believe one of the potential uses of the techniques we've seen previously involves creating a functionality layer that acts as a library, independent of data and could be utilized anywhere.

Let's look at the requirement: we want to assign a series of products to certain categories. Note that the relationship between products and categories is a many-to-many relationship; we can have a product in multiple categories, and a category can have multiple products:

Article content

Let's start by identifying the classes I will create, beginning with the wrapper for the categories.

  1. Category is the wrapper for ProductCategory. We could have chosen to call it ProductCategoryClass to maintain consistency with the previous example, but I aim to show another possibility.
  2. I want to "assign" certain products to a list of categories. This tells me what behavior I intend to encapsulate. See the method assign; assignProducts could have been another name, but the fact that it receives products as a parameter implies that what I'm trying to assign are products.
  3. Finally, I create a method that is actually a utility, the newProdCategoryJunction method creates the junction or "intermediate" object.

Article content

While we have identified part of the solution, the requirement asks us to assign products to a set of categories. So:

  1. In the categories class, I have the assign method, but inside I use the method provided by the Category class.

Article content

We have two classes where I need to be able to insert those ProductCategoryProduct objects so that I can associate said products with the corresponding categories (many-to-many relationship).

In the next case, it might be tempting to execute the DML statement within the method, and in some cases, it might be sufficient for the solution you are looking for. In others, assigning the products to a category and having these records might only be a part of what we need in a transaction, so inserting the records might not be an option yet.

Article content

In this case, we face the possibility of having to duplicate our logic for cases where I need to perform the insertion of records. Another solution could be to pass a boolean as a parameter called doInsert or something similar. However, such solutions start to break the logical unity of our methods; now we have methods that perform more tasks. A solution to this can be found in the book Elegant Objects vol.2 by Yegor Bugayenko.

4.5. Decorators!

Let's pause before we start, the following technique uses the decorator pattern, and I want to clarify that "It's not something you have to do!" While the use of design patterns can bring benefits like better structure and division of responsibilities, it also introduces complexity. In this case, if you decide to use the design pattern, your team should at least be able to look at the code and understand what is happening. I will do my best to explain the following design pattern and why it might be useful (although maybe that's for another blog).

I like to see decorators simply as one of those robots from Avatar. We get inside the robot, we can walk, grab objects, we can communicate, but all this is enhanced 10x times.

Article content

When we look at the decorator pattern, what's happening is the same, we put one object (the object to be decorated) inside another (the decorator). In fact! The method we have in both objects will be the same, but the decorator will handle executing additional logic before and after.

In Yegor Bugayenko's book, Elegant Objects, there are very good examples of this. Let's say you want to read a book.

Article content

But let's say it's a "sensitive" book, maybe something confidential that not everyone should read. One of the first solutions might be to simply add the logic to verify who is reading it inside the read method, although this could significantly increase the functionality of read, maybe involve bugs that we can't foresee. So a solution would be to use decorators.

Article content

Notice that in both cases, we are still talking about a book! It's just that we've added extra functionality by "embracing" the normal book with the decorator. Let's see what the read method of the Book does:

Article content

While the secureBook does the following! Note that we are calling the method from above, the one from the normal book. This last part is possible because, if you remember, we passed the normal book inside the secure book.

Article content

To implement the design pattern, an interface is used. The interface defines which method to decorate. If you look at the example above, notice that in both cases we always use the read method, so that would go in the interface.

4.6. Decorators in Salesforce.

One of the uses we can give these decorators in Salesforce is for access control and it would be available the option to execute DML statements within the wrapper classes we've used through the decorator.

In the following class, we are performing the assignment of products to a category. We insert the products and return the assignment (this can be optional depending on what you are looking for).

Article content

If we need to structure such database insertion or validate access to Field Level Security (FLS) or the object, then we can create decorators. We have the interface that will allow us to decorate our main class, which will perform the insertion. Note that the method I will decorate is assign to perform the insertion (and subsequently validate security).

Article content

Now we have the decorator, which will simply call the same assign method on the decorated class, which is Categories, since Categories does all that processing, now I am able to simply call it and perform the insertion from here.

Article content

Notice that I can introduce more logic like this implementation that checks the security and access to the Salesforce object, this could be in another decorator!

Article content

Notice that I now use ICategories which emerged as a result of encapsulating the normal Category behavior within CategoriesDB.

Article content

One of the greatest advantages of this approach to the problem is not only the structure but the definition of layers within the code. It is this definition in layers that allows us to distribute complexity, reuse code.

Article content

Another advantage of having this structure is that we have defined a layer, the fact that it is decoupled from the database allows us to do many things, facilitates testing, and allows us to move this layer from one organization to another that has the objects we want so that a library can be created, in the worst case, you will have functionality that you do not use (yet).


5. Folder Structures.

So far, we have observed singular classes like an Account, collection classes like Accounts, and ultimately even a design pattern that introduced an IAccount and an AccountsDB! The complexity has grown, and in many cases, you won't need to reach the design pattern stage—it's just one more option, and whether or not you choose it will depend greatly on the circumstances you find yourself in!

But if it were the case that you need to use it and inevitably the complexity increases, you might find yourself needing to implement a more complex folder structure that tries to be self-explanatory.

This is an option that allows us to keep classes related to an object together. If you decide to follow a technical approach to classes, you could now have a folder with batch classes that utilize the logic. For example.

Article content

Or in the case that you prefer to later package, or start using a structure like screaming architecture, any option you decide is good as long as it is organized in a folder structure, plus we know that much of the logic of our transactions with the objects we work with are within the main objects.


6. Last thoughts

We have covered a wide range of topics so far, from how SOQL resembles an ORM to the challenges related to the inability to perform DML statements within for loops. We've also explored the need to use complex data structures and some potential solutions to these problems using the object-oriented paradigm.

I want to reiterate that my intention is to offer a different way of thinking about code, which may or may not be useful depending on your situation. Many organizations face significant challenges with code management, many opt for libraries, many choose to continue with their current development model, and that's okay!

However, I do not believe that just because something works means it is the end of the road; it is just the beginning of continuous improvement. While for some projects an AuraEnabled method that fulfills its function may be more than enough, we face the question of what to do when it is not enough. A year ago, I still questioned a lot why there was so much code in Salesforce that was not object-oriented. What are the reasons and motives for its non-existence? Maybe it's too complex? Or maybe it's not known? Many times I was given answers, but I still couldn't find the reason why things that worked in certain environments couldn't work in Salesforce. In researching, I came across the UnitOfWork and thought everything was solved! But surprise, it wasn't! It was a good tool, and I could use it in some cases, but it guaranteed nothing (yet I invite everyone to research the open source in Salesforce and many other incredible libraries that the community has made).

What you read here is the result of several months of work. I sincerely hope that you can take advantage of some of what has been discussed. As Salesforce developers and architects, it is crucial to pay attention to the design process we implement, as the structure we choose directly influences maintainability and, therefore, the costs to the client.

This article is not intended to be the last word. Instead, I seek to invite more people to comment and share their opinions, hoping that together we can come to better solutions over time. Programming in Salesforce, like on any platform, does not have universal solutions, often a procedural solution or a method that simply returns what it should is enough, but when it is not, what options do we have?

I hope this is just the beginning of something more, that helps us all improve our practices and find more effective and efficient solutions. Collaboration and the exchange of ideas are fundamental for our evolution as developers. I do not intend for this to end here! (Maybe I should have called this initial thoughts).

Great article... Taking Salesforce programming to the next level. I'm not a developer but I enjoyed the reading and learned a lot. Thank you, Jorge Teodoro Dawn Rodriguez!

Like
Reply

This is an awesome article!

If you're wondering how to maximize your company's potential with Salesforce, you've come to the right place! In our blog, "What does a Salesforce consulting agency do?", we break down everything you need to know about how a Salesforce consulting agency can transform your organization. https://rootstack.com/en/blog/what-does-salesforce-consulting-agency-do

Like
Reply

What a great article Jorge Teodoro Dawn Rodriguez ! I loved it. Anyone developing on Apex should read it.

Very insightful! Thanks for posting!

To view or add a comment, sign in

More articles by Jorge Teodoro Dawn Rodriguez

Others also viewed

Explore content categories