Bringing Meaning to Legacy Code with Parameter Objects

Most of the legacy codebases I have work on suffered from primitive obsession. One of the symptoms is the presence of methods with a swarm of int, string or bool parameters. These methods harm the codebase as they are hard to understand and easy to misuse - which could cause bugs and headaches. This article aims to present a simple, yet extremely effective, solution to this issue through the use of parameter objects.

Let’s take the following method signature to illustrate:

string GenerateLoginLink(
    int userId, 
    DateTime? expiryDate, 
    int? maxAllowedClicks, 
    bool notifyOnClick, 
    bool sendLinkByEmail, 
    bool redirectToHomePage, 
    string? redirectUrl

This method generates a link that would allow the user to log in to the platform and be redirected to a specific url. This link has an expiry date or a maximal number of clicks allowed. Also, we can optionally send it directly to the user by email and notify an admin when the user has clicked on it.

There are a couple of business rules to be aware of:

  • expiryDate and maxAllowedClicks are mutually exclusive (i.e. we can only pass one of them as an argument)
  • redirectUrl is only useful if redirectToHomePage is false

Now, this method presents some issues.

  • Illegal combinations. In the best-case scenario, providing the method with an expiryDate and a maxAllowedClicks throws an exception or silently gives priority to one of these fields. In the worst-case scenario, it generates an unpredictable behaviour because the case is not handled. Also, what if both are null?
  • Poor expressivity. The business rules expressed above are far from obvious. Maybe they are documented in the method’s comment but people do not always read the documentation and it can become stale very quickly if we forget to update it (maybe that is the reason why people tend to overlook documentation?). Also, there is absolutely nothing preventing the developer to pass illegal or useless combinations.
  • Error-prone. Finally, did you notice the 3 consecutive booleans in the method’s signature? How easy would it be to invert two of them by mistake? The method might work exactly as expected and be thoroughly unit tested, you will still have bugs if you don’t use it properly.

The first iteration would be to use a parameter object for this method.

public class LoginLinkConfig 
    public DateTime? ExpiryDate {get; set;}
    public int? MaxAllowedClicks {get; set;}
    public bool NotifyOnClick {get; set;}
    public bool SendLinkByEmail {get; set;}
    public bool RedirectToHomePage {get; set;}
    public string? RedirectUrl  {get; set;}

string GenerateLoginLink(int userId, LoginLinkConfig config);

This change could reduce the risk of error and would make the use of default values easier. However, it would not be very helpful to prevent illegal combinations and increase expressivity.

A solution to tackle these issues is to use the builder pattern. This pattern provides an expressive API to create an object and is particularly useful when it comes to objects with optional fields.

public class LoginLinkConfig 
    // Properties are now read-only
    public DateTime? ExpiryDate {get;}
    public int? MaxAllowedClicks {get;}
    public bool NotifyOnClick {get;}
    public bool SendLinkByEmail {get;}
    public bool RedirectToHomePage {get;}
    public string? RedirectUrl  {get;}

    // The constructor is private to force the use of the builder
    private LoginLinkConfig(DateTime? expiryDate, int? maxAllowedClicks, bool notifyOnClick, bool sendLinkByEmail, bool redirectToHomePage, string? redirectUrl) 
        ExpiryDate = expiryDate;
        MaxAllowedClicks = maxAllowedClicks;
        NotifyOnClick = notifyOnClick;
        SendLinkByEmail = sendLinkByEmail;
        RedirectUrl = redirectUrl;

    // Notice that the parameter in these methods is not nullable
    public static Builder ExpiringLink(DateTime expiryDate) => new Builder(expiryDate);
    public static Builder LimitedClicksLink(int maxAllowedClicks) => new Builder(maxAllowedClicks);

    public class Builder
        private DateTime? _expiryDate;
        private int? _maxAllowedClicks;
        private bool _notifyOnClick;
        private bool _sendLinkByEmail;
        // We can set sensible default values
        private bool _redirectToHomePage = true;
        private string? _redirectUrl;

        public Builder(DateTime expiryDate)
            _expiryDate = expiryDate;

        public Builder(int maxAllowedClicks)
            _maxAllowedClicks = maxAllowedClicks;

        public Builder WithRedirection(string redirectUrl)
            _redirectToHomePage = false;
            _redirectUrl = redirectUrl;

            // Return the builder to allow chaining
            return this;

        public Builder WithClickNotification()
            _notifyOnClick = true;

            // Return the builder to allow chaining
            return this;

        public Builder SentByEmail()
            _sendLinkByEmail = true;

            // Return the builder to allow chaining
            return this;

        public LoginLinkConfig Build() => new LoginLinkConfig(_expiryDate, _maxAllowedClicks, _notifyOnClick, _sendLinkByEmail, _redirectUrl);


With this builder, creating the config object goes from

var maxAllowedClicksConfig = new LoginLinkConfig 
    MaxAllowedClicks = 1,
    NotifyOnClick = true,
    RedirectToHomePage = false,
    RedirectUrl  = "/myaccount"

var expiringLinkConfig = new LoginLinkConfig 
    ExpiryDate = DateTime.Now.AddDays(1),
    SendLinkByEmail = true,
    RedirectToHomePage = true


var maxAllowedClicksConfig = LoginLinkConfig.LimitedClicksLink(1)

var expiringLinkConfig = LoginLinkConfig.ExpiringLink(DateTime.Now.AddDays(1))

This new instantiation process is arguably more verbose, but provides the following benefits:

  • More meaningful. By using a builder, the developer can express what they mean and there is no doubt about it. Since we got rid of the ambiguity, errors are less likely to occur.
  • No illegal state. As illustrated in the example, there is absolutely no way of representing an illegal state. For instance, if you want to create an expiring link, you cannot also set the _maxAllowedClicks. Otherwise, when you provide a redirectUrl, the _redirectToHomePage is automatically set to false and cannot be changed.
  • Simpler implementation. Finally, since the business rules described above are embedded into the object creation, there is no need to enforce them into the method - which allows for simpler implementation. Also, other methods can use the config object, knowing that the business rules will always be applied.

Overall, using a parameter object in conjunction with the builder pattern is a very simple way to bring back meaning into your legacy codebase and reduce bugs by making illegal states literally impossible to represent.