Reduce, Reuse, Refactor: Object Variables
Posted: March 15, 2025

Reduce, Reuse, Refactor: Object Variables

Table of Contents:

  • Definining Class Variables - AKA Fields

  • Annotations

  • Properties, Getters & Setters

    • JSON - Text To Transmit Data
    • A Special Note On @AuraEnabled For Properties
    • Introducing Encapsulation
    • Lazy Loading
  • Singleton Variables

  • Conclusion

Where you are in the Reduce, Reuse, Refactor series:

Methods are how objects communicate with one another; variables are how objects store data. You’ve already seen several examples of variables in the previous posts:

public abstract class RetryMechanism {
  // static field as a class variable
  private static final Integer DEFAULT_RETRY_CAP = 5;
  // instance field as a class variable
  private final Integer retryCap;

  public RetryMechanism(Integer retryCap) {
    this.retryCap = retryCap;
  }

  // etc...
}

Definining Class Variables - AKA Fields

Let’s dive right in to the syntax for declaring variables. Though there is some flexibility with the ordering for some of these rules (for example, 1 and 2 can be used in interchangeable order), it’s a best practice to always follow the same order: consistency makes a codebase easier to read.

  1. Visibility modifier: “global”, “public”, “protected” or “private” (same as methods)
  2. static keyword: going back to the “prototype” example from On The Relationship Between Automation & Code, a static field is only initialized once and stays consistent, regardless of how many instances of a class you are working with
  3. final keyword: most commonly used with instance variables for a class (though it can be used to qualify method arguments as well as variables within methods, too), the final keyword says “this field can only be initialized where it’s declared, or once within an object’s constructor
  4. transient keyword: prevents a field or property from being serialized — we’ll be talking more about serialization, and JSON, later on in this post
  5. the type of the field you’re declaring: while Object is always allowable, since all objects ultimately descend from it, this is typically an actual declared type (like the Integer declared in the example above)
  6. the name of the field: this is how you’ll reference it within the class in question
  7. the equals sign, which means “all of the stuff we just covered is being assigned using the logic on the right of me”
  8. the initialization mechanism: typically the new keyword, but it could be any of the ways that we’ve covered when it comes to object construction

Let’s run through a few field examples:

public class VariablesExample {
  public final Integer one = 1;

  public static Integer CURRENT = 0;

  // at some point after reading this series, you should look back and realize
  // how wild this statement actually is
  private final Integer two = (Integer) JSON.deserialize('2', Integer.class);

  public void printTwo() {
    System.debug(this.two);
  }
}

// and then in something like Anonymous Apex:
VariablesExample example = new VariablesExample();
// prints out 1
System.debug(example.one);

// we refer to static variables using the type name,
// not any particular instance of VariablesExample
// like the "example" variable, above
// prints out 0
System.debug(VariablesExample.CURRENT);
// prints out 2
example.printTwo();
// prints out System.FinalException: Final variable has already been initialized
example.one = 7;

Annotations

Like Apex classes, variables can also be annotated in a variety of ways. Annotations are always PascalCase. Annotations enhance pre-existing behavior when it comes to variables, and they largely behave the same way regardless of what the annotation in question is enabling. I’ll once again cover only what’s strictly necessary here, rather than walking through the exhaustive list of possible annotations:

  • @AuraEnabled exposes a variable to the frontend — we’ll talk more about some special behavior when it comes to @AuraEnabled properties very soon
public class Example {
  @AuraEnabled
  public final String label = 'Some label';

  @AuraEnabled
  public static Example getExample() {
    // on the frontend, this returns an object with a field called "label"
    // that's always initialized to "Some label":
    // { "label": "Some label" }
    return new Example();
  }
}

Notice that this example is extremely similar to the getText one from On The Relationship Between Automation & Code! There’s very little difference between using an @AuraEnabled annotated method versus an @AuraEnabled field, and the latter is much more common.

  • @TestVisible exposes a variable of protected or private visibility so that tests can reference it. Using @TestVisible is largely considered an anti-pattern; we’ll get more into this later on in the series, but the best case scenario is always for your production-level code to have no knowledge of your tests
public class Example {
  // this annotation allows tests to reference Example.SOME_CONSTANT
  // even though it's technically private
  @TestVisible
  private static final String SOME_CONSTANT = 'some value only this class needs to know about';
}

Looks like this will be a short post, right? Object fields and annotations are a fairly easy subject to ease into. But of course, even though the basic syntax for fields is easy to understand and read, there’s always some hidden complexity — with variables, this primarily comes in the form of a special kind of variable, otherwise known as object properties.

Properties, Getters & Setters

Unlike standard fields, properties follow a specific syntax that deviates from the rules outlined above: instead of using the equals sign for assignment like a field, variables with getters and setters start to look a bit like classes in that they use the curly brace notation:

public class Example {
  public Integer one {
    get;
    set;
  }

  // the simplest get/set property
  // works just like any other variable
  public Example() {
    this.one = 1;
  }
}

Also note that it’s not required that a property have both a getter and a setter. Not having a setter is much more common, but in general it’s very uncommon to see properties without both keywords being used.

Typically, properties aren’t going to be used just with a get/set (outside of a special case with @AuraEnabled that we’ll cover); they’re used because they can do things that fields can’t. Like this:

public class Example {
  public Integer one {
    get;
    // by default, get/set have the same
    // visibility as the property they're attached to
    // but you can reduce the visibility of the setter
    // to prevent a property from being updated outside of a class
    private set;
  }

  public Example() {
    this.one = 1;
  }

  public void updateOne(Integer value) {
    this.one = value;
  }
}

// and then in another class
Example example = new Example();
// outputs 1
System.debug(example.one);
// fails to compile with the message:
// Variable is not visible: Example.one (34:8)
example.one = 2;

example.updateOne(2);
// ouputs 2
System.debug(example.one);

So reducing visibility is one way that data stored in a class can be kept safe — because sometimes it’s convenient to expose variables to the “outside world”, but it’s almost never a good idea to allow other classes to update a field without using something like updateOne. We’ll get more into the specifics about that in the Encapsulation section of this post. For now…

There are many reasons why we allowing other objects to re-assign the public fields on another object is considered poor form, but a simple reason to start with is that it helps to build up a mental model of the system you’re building or maintaining when methods are the way that objects send messages. Mutating variables — by re-assigning them directly — is really only suitable when you’re working with something called a POJO, or “Plain Old Java Object” (sometimes also referred to as a DTO, or “Data Transfer Object”):

public class ExamplePojo {
  // getters/setters are kept on the same line as the property
  // when there isn't any additional logic being performed inside of them
  public Integer firstVariable { get; set; }
  // which means that in a POJO, you can use field declarations
  // interchangeably with properties:
  public Integer secondVariable;
}

I’d make the argument that when people use properties in a POJO/DTO, they’re doing so without entirely understanding why properties have a place in their “plain old object”. But in order to fully explain that sentence, we also have to discuss JSON, or JavaScript Object Notation.

JSON - Text To Transmit Data

In JavaScript, objects look like this:

const myObject = {
  firstVariable: 1,
  secondVariable: 2,
};
// if we turn this object into JSON, it looks like this:
console.log(JSON.stringify(myObject));
// outputs
// {"firstVariable":1,"secondVariable":2}

When data is transmitted between somebody’s browser and a backend system, one of the easier ways to do that is through text. On the other side, though, omitting types isn’t convenient. We want to know what sort of object we’re working with at basically all times. That’s where serialization (the process of turning an object into text — which is always a String type in Apex) and deserialization come into play. The process of deserializing something is to take an object represented by text and transpose it back to a concrete class instance. For example:

String exampleJson = '{"firstVariable":1,"secondVariable":2}';

ExamplePojo deserializedInstance = (ExamplePojo.class) JSON.deserialize(exampleJson, ExamplePojo.class);
// outputs: 1
System.debug(deserializedInstance.firstVariable);
// outputs: 2
System.debug(deserializedInstance.secondVariable);

So far so good, right? One of the places that properties really shine, though, when compared to fields, is when deserializing specific sorts of data:


public class ExampleBoolean {
  public Boolean isNullOrUndefined;

  public ExampleBoolean() {
    this.isNullOrUndefined = false;
  }
}

String exampleJson = '{ }';
ExampleBoolean instance = (ExampleBoolean) JSON.deserialize(exampleJson, ExampleBoolean.class);
// outputs null
System.debug(instance.isNullOrUndefined);

Taken as a field, isNullOrUndefined suffers from a possibly fatal flow — Booleans in Apex should only have values of true or false, but deserializing to an instance of an object does not call that object’s constructor. One reason null booleans in particular end up as unfortunate bugs in Apex is because conditional statements don’t allow for nulls by default:

// throws with: System.NullPointerException: Attempt to de-reference a null object
if (instance.isNullOrUndefined) {
  System.debug('false');
} else {
  System.debug('true');
}

Oops! Believe it or not, this is a common error that trips people up! And that’s OK — we all make mistakes. But let’s forgive ourselves and come back around to properties with this information in mind. You see, in addition to reducing visibility, properties have a superpower that elevates them above fields for certain kinds of behavior:

public class ExampleBoolean {
  public Boolean isNullOrUndefined {
    get {
      // self-referencing the property that you're in
      // is a peculiar language feature, but one that
      // enables some pretty awesome behavior
      if (this.isNullOrUndefined == null) {
        this.isNullOrUndefined = true;
      }
      return this.isNullOrUndefined;
    }
    private set;
  }
}

String exampleJson = '{ }';
ExampleBoolean instance = (ExampleBoolean) JSON.deserialize(exampleJson, ExampleBoolean.class);
// outputs: true
System.debug(instance.isNullOrUndefined);

// let's look at that example again, this time
// using a newer language feature - the "??" null coalesce operator -
// to simplify the getter:
public class ExampleBoolean {
  public Boolean isNullOrUndefined {
    get {
      // the "??" null coalescing operator says:
      // "use the value on the right if the value on the left is null"
      this.isNullOrUndefined = this.isNullOrUndefined ?? true;
      return this.isNullOrUndefined;
    }
    private set;
  }
}

In short, because properties can run logic during instantiation and deserialization, they feature more power than standard fields. And while other languages require properties to use backing fields when employing custom getters and setters, in Apex we get to use the self-referential version of a property to do things like checking for null, which cuts down on the boilerplate for initializing properties. And we need to take all of the succinctness we can get when it comes to properties, because the braces and new lines required to make everything look good in a codebase means that every new property with at least a custom getter (which is more typical than custom setters) will end up taking up at least 7 lines. That’s seven times more space than a field, which takes only a single line to declare!

Both getters and setters can use code blocks to employ logic. Setters employ a “magic” argument called value — “magic”, because it doesn’t appear explicitly as an argument:

public class MagicSetter {
  public Boolean alwaysTrue {
    get;
    set {
      if (value) {
        alwaysTrue = value;
      } else {
        throw new IllegalArgumentException(value.toString() + ' is invalid for this property!');
      }
    }
  }
}
MagicSetter setter = new MagicSetter();
setter.alwaysTrue = true;
// outputs: true
System.debug(setter.alwaysTrue);
// throws: System.IllegalArgumentException: false is invalid for this property!
setter.alwaysTrue = false;

Remember that fields and properties that are marked using the transient keyword won’t show up in JSON.

A Special Note On @AuraEnabled For Properties

It’s now time to revisit the @AuraEnabled annotation, because (as mentioned earlier in this chapte) it’s one of the few times it makes sense to use the one-line version of getters and setters — while it’s perfectly valid to return fields to the frontend using the @AuraEnabled annotation, only properties can be deserialized from JavaScript objects back into Apex:

// some JavaScript code that calls Apex:
const example = { first: "Hello", second: "World" };
await myMethod({ example });

And then in Apex:

public class Example {
  public String first { get; set; }
  public String second { get; set; }

  public static void myMethod(Example example) {
    // outputs: Example:[first="Hello" second="World"]
    // but this only works because first and second are properties and not fields
    System.debug(example);
  }
}

So that’s fun.

Introducing Encapsulation

Remember when I said that methods are how objects communicate? In programming, there are exceptions to most rules. While it’s true that by far the most common sort of public variable associated with objects are fields that are denoted as public final — which effectively gaurantees that the data being stored by each object instance can’t be tampered with — there are occasionally times where the author of a class wants to avoid writing additional methods for the class they’re working with. Typically this is because they have logic that should remain within the class in question responsible for the setting of a field. The idea that objects should be self-contained, and that they should effectively “hide” their implementation details when being used, is commonly referred to using the principle of Encapsulation. Therefore, occasionally you’ll see objects foregoing the final keyword on certain properties, and employing custom setters to call private methods from within the class. This allows people using the class (commonly referred to as consumers, sometimes bundled with the word downstream in keeping with the concept of a hierarchy of program flow) to use public properties as fields, while still benefitting from validation logic to alert them to improper usages (like the System.IllegalArgumentException being thrown, above).

In many ways, we’ve been dancing around the concept of encapsulation this whole time. It’s the central tenet that influences the authoring of objects. Objects should represent meaningful concepts. They should have both outward facing (public) ways to be interacted with, but should also only expose publicly or globally the methods and variables that are absolutely essential for the rest of the program to function. There are many schools of thought on exactly how to create and author objects. Even methodologies that at first may seem completely opposed to traditional Object-oriented programming tend to actually use core concepts like encapsulation within their own doctrines. Take, for example, the functional programming paradigm of Domain-Driven Design (DDD). While DDD is in and of itself too much subject material to cover even in brief here, its central tenet is that business logic in the system should map exactly to concepts that are understandable to non-programmers within the business. If somebody in Finance, in other words, uses a process called “reconciliation” when mapping revenue to outlays, functions associated with that part of the system should use “Reconcilation” and its associated terminology. The naming of objects in an Object-oriented system can (and perhaps should) employ this same principle as much as possible. In other words — the greater the overlap between the names of objects that you create and use within the business domain itself, the easier it will be to explain the system and to talk with the subject matter experts responsible for using the system you’re building.

Alright! Let’s return to exploring properties to see another way in which they offer advantages over fields.

Lazy Loading

One other way in which properties distinguish themselves from ordinary fields is through their support for lazy loading. When we create new instances of objects, the data in its variables has to be allocated. Allocations get put on the heap — and our programs have finite amounts of heap space. This is an area where Salesforce programs tend to differ substantially from other programs, where you typically have the option to increase the amount of memory needed to run your application. At the time of writing — and for the last two plus decades — Salesforce developers haven’t had that luxury. Because heap space is limited, we as engineers have a duty to the future scalability of the programs we author to ensure our objects don’t balloon in the amount of heap space they take up.

Considers the concept of constants within Apex. One extremely common use case for constants is when referring to picklist values within code:

public class AccountPicklists {
  public static final Industry Industry = new Industry();

  public class Industry {
    public final String Agriculture = 'Agriculture';
    public final String Apparel = 'Apparel';
    // etc ... all the way to
    public final String Transportation = 'Transportation';
    public final String Utilities = 'Utilities';
    public final String Other = 'Other';
  }
}

// then, elsewhere in the code, we can easily refer to the constant version of an Industry
Account acc = new Account();
acc.Industry = AccountPicklists.Industry.Other;
// do something with the account

Remember when I said to stay tuned at the end of On The Relationship Between Automation & Code for a cool inner class example? Well, here we are! Encapsulating related classes within one “outer” class is a great way to broadcast information to the rest of the system — this isn’t just any Industry, it’s something related to Accounts in particular.

There’s just one catch, and Account.Industry is a great example of it. By default, that picklist contains thirty two values! Each one of those strings needs to be allocated to the heap. If Industry is the only picklist on Account that you care about, that might be fine. But what if you’re also Account.Ownership? Naively, that might look like this:

public class AccountPicklists {
  public static final Industry Industry = new Industry();
  public static final Ownership Ownership = new Ownership();

  public abstract class ContainsOther {
    public final String Other = 'Other';
  }

  // now each of these subclasses features the Other property
  public class Industry extends ContainsOther {
    private Industry() {
      System.debug('Initializing industry');
    }
    public final String Agriculture = 'Agriculture';
    public final String Apparel = 'Apparel';
    // etc ... all the way to
    public final String Transportation = 'Transportation';
    public final String Utilities = 'Utilities';
  }

  public class Ownership extends ContainsOther {
    private Ownership() {
      System.debug('Initializing ownership');
    }
    // public and private are reserved words in Apex
    // so we need to use slightly different names
    // for these variables
    public final String PublicValue = 'Public';
    public final String PrivateValue = 'Private';
    public final String Subsidiary = 'Subsidiary';
  }
}
// outputs THREE debug messages:
// Initializing industry
// Initializing ownership
// Other
System.debug(AccountPicklists.Industry.Other);

So that’s perhaps a little unexpected. We were only referencing the Industry inner class, but because we referenced the AccountPicklists class, and because both the Industry and Ownership fields on that class are listed as final, both of them were initialized upon first being referenced. Sometimes that’s OK! But just as the concept of encapsulation revolves around the idea that objects should only “show” methods and data (which we’ll hereafter refer to as their “API”, or Application Programming Interface, also commonly referenced as the “shape” of an object), so too does the concept of intentionality command our decision-making process when it comes to the creation of properties versus fields. As a reader of the AccountPicklists class, for example, the decision to make both Industry and Ownership fields has a ripple effect on the system. We should read that decision as saying something to the effect of “yes, everything gets loaded for this object by default, and that’s done on purpose.”

Intentionality is one of the great force multipliers you can have as an effective engineer. If you’re consistent in how you author and think about objects, your consistency helps others to reason about and commit to memory what you meant. I cannot underscore enough how powerful this concept is. When you read code that is written intentionally, it’s much easier to inhabit the previous head space that the prior author — which frequently is simply a past version of yourself — was thinking. The problem is that many people don’t study these core Object-oriented concepts; they don’t understand the principles of the language they purport to use. Words matter; so does intention. Getting into the habit of incorporating intentionality into your programming is, therefore, one of many ways in which you can use Object-oriented programming to uplevel your career.

So let’s look at how using properties over fields allows us to lazy-load only the things we need:

public class AccountPicklists {
  /**
   * The fields have been upgraded to properties
   * */
  public static final Industry Industry {
    get {
      this.Industry = this.Industry ?? new Industry();
      return this.Industry;
    }
    private set;
  };
  public static final Ownership Ownership {
    get {
      this.Ownership = this.Ownership ?? new Ownership();
      return this.Ownership;
    }
    private set;
  };

  // The rest of this is the same
  public abstract class ContainsOther {
    public final String Other = 'Other';
  }

  // now each of these subclasses features the Other property
  public class Industry extends ContainsOther {
    private Industry() {
      System.debug('Initializing industry');
    }
    public final String Agriculture = 'Agriculture';
    public final String Apparel = 'Apparel';
    // etc ... all the way to
    public final String Transportation = 'Transportation';
    public final String Utilities = 'Utilities';
  }

  public class Ownership extends ContainsOther {
    private Ownership() {
      System.debug('Initializing ownership');
    }
    // public and private are reserved words in Apex
    // so we need to use slightly different names
    // for these variables
    public final String PublicValue = 'Public';
    public final String PrivateValue = 'Private';
    public final String Subsidiary = 'Subsidiary';
  }
}
// outputs TWO debug messages:
// Initializing industry
// Other
System.debug(AccountPicklists.Industry.Other);

When we use getters to lazy load properties, we’re broadcasting our intentionality to others: yes, this object may contain a lot of data, but we’re treating the heap right by only loading what we need. That’s a nice thing to be able to pick up on.

Singleton Variables

There are many patterns that can be ascribed to programming; most famously, using the so-called “Gang of Four” book titled “Design Patterns”. There is also the phenomenal resource Refactoring Guru which heavily features the design pattern concepts explored by the original Gang of Four book. One of the most common patterns used in programming is the Singleton pattern, and because we’ve now covered properties and lazy loading, this is a great chance to talk about it. Singletons force downstream consumers to only ever use a single instance of the class as an object — no matter how many times that object is referenced or assigned, only a single instance can be accessed.

This dovetails nicely from our discussion about intentionality with respect to heap size above, as singletons are often used to optimize heap usage or CPU time (in the event that the initialization of the object is time-consuming, perhaps because of data being fetched). In fact, you’ve already seen two examples of inner classes as singletons, since the AccountPicklists.Industry and AccountPicklists.Ownership constructors are marked as “private.” That means consumers can only access the singular instance of those classes that’s exposed via the lazy loaded public properties.

When we’re creating a singleton for an outer class instance, it follows the same rules as those inner classes:

  • the constructor must be marked as private
  • an instance of the class must be exposed via some kind of public method and/or property

For example:

public class SingletonExample {
  public static final SingletonExample Instance {
    get {
      Instance = Instance ?? new SingletonExample();
      return Instance;
    }
    private set;
  }

  public static SingletonExample getInstance() {
    // recall that static properties aren't prefaced
    // with "this."
    return Instance;
  }

  public Account getMagicAccount() {
    return new Account(Name = 'Magic');
  }

  private SingletonExample() {
    this.expensiveCpuTimeProcedure();
  }

  private void expensiveCpuTimeProcedure() {
    // here we'll just stub something out that takes four seconds
    Datetime nowish = System.now();
    // it's uncommon to use a semi-colon instead of the braces
    // for something like this, but it's important to know that
    // the language supports this syntax
    while (System.now() < nowish.addSeconds(4));
    System.debug('Loading complete');
  }
}

// and in usage, outputs:
// (... after four seconds) Loading complete
// Account:{Name=Magic}
System.debug(SingletonExample.Instance.getMagicAccount());
// immediately outputs: Account:{Name=Magic}
// because now we've already loaded the singleton into memory
// and "paid the time tax" so to speak
System.debug(SingletonExample.getInstance().getMagicAccount());

Although typically a singleton is only exposed via either a method or a static property, I’ve included both in this example to highlight how they each work. But hopefully this example serves to highlight how using properties combined with programming concepts can help us to create a more performant, informative system.

Conclusion

In this post, we learned about simple fields and the complicated truth behind properties. We got to see how to self-refer to properties, how JSON and deserialization work together, and started diving into the deep topics of encapsulation and design patterns. In short — we learned a lot, and that was all while covering only two relatively small concepts behind variables. Because variables are central to how objects work, this is time well-spent.

It’s important to remember that the goal isn’t simply to encapsulate data when we make objects; rather, it’s to model concepts and business logic. Likewise, objects don’t need to be complex to represent complex things — we saw this with the AccountPicklists example, which explored the power that naming things can have while creating type-safe constants within a system. It’s not that these examples purposefully avoided the use of methods; it’s that encapsulation is all about striking the balance between how to send messages, and how to access data. Now that you know about variables, we’ve essentially covered the basics behind object creation and the syntax that drives objects. Next up, we’ll talk about object hierarchies and inheritance, which will allow us to start taking a look at more complicated examples like polymorphism.

In the past three years, hundreds of thousands of you have come to read & enjoy the Joys Of Apex. Over that time period, I've remained staunchly opposed to advertising on the site, but I've made a Patreon account in the event that you'd like to show your support there. Know that the content here will always remain free. Thanks again for reading — see you next time!

Amazon author
LinkedIn
Github
James Simone

Written by James Simone, Principal Engineer @ Salesforce, climber, and sourdough bread baker. For more shenanigans, check out She & Jim!

© 2019 - 2025, James Simone LLC