# Encapsulate Field

> Make a public field private and give clients accessor methods, so the class controls how its data is read and changed.

- kind: technique
- category: organizing-data
- aliases: Encapsulate Variable
- canonical: https://aipatterns.dev/refactoring/techniques/encapsulate-field
- inspired-by: https://refactoring.guru/encapsulate-field

## Problem

A class exposes a public field. Any code can read it or assign anything to it, so the class cannot enforce rules, react to changes, or later change how the value is stored.

## Solution

**Make the field private, add a getter and (only if needed) a setter or an intention-revealing method, and change all clients to use them.**

## Before

```ts
class ApiKey {
  public scopes: string[];
  public expiresAt: Date;
  public revoked = false;

  constructor(readonly id: string, scopes: string[], expiresAt: Date) {
    this.scopes = scopes;
    this.expiresAt = expiresAt;
  }
}

// elsewhere
key.revoked = true;
key.expiresAt = new Date(0);
if (!key.revoked && key.expiresAt > new Date()) authorize();
```

## After

```ts
class ApiKey {
  private _revokedAt: Date | null = null;

  constructor(
    readonly id: string,
    readonly scopes: readonly string[],
    private readonly _expiresAt: Date,
  ) {}

  get expiresAt(): Date { return new Date(this._expiresAt); }
  get revoked(): boolean { return this._revokedAt !== null; }

  revoke(now: Date): void {
    this._revokedAt ??= now;
  }

  isActive(now: Date): boolean {
    return !this.revoked && this._expiresAt > now;
  }
}

// elsewhere
key.revoke(new Date());
if (key.isActive(new Date())) authorize();
```

## Why

- A key can no longer be un-revoked or have its expiry silently rewritten.
- The class now records when revocation happened, a change invisible to clients.
- Behavior that used the fields (`isActive`) can move into the class.

## Steps

1. Add a getter (and setter, if clients assign the field) with the same name or a clear one.
2. Find every client read and replace it with the getter. Run the tests.
3. Find every client write. Replace it with the setter or, better, with a method named for the intent (`revoke()`, not `setRevoked(true)`). Run the tests.
4. Make the field private. Compile to catch any remaining direct access.
5. Remove the setter if no client needs it.

## Watch out

- Getters that return mutable objects (arrays, dates, maps) still let clients modify internals. Return copies or read-only views; for collections use [Encapsulate Collection](https://aipatterns.dev/refactoring/techniques/encapsulate-collection.md).
- Plain data carriers (DTOs, API request types, config records) don't need encapsulation. Public `readonly` fields are fine there.
- In TypeScript and C#, property accessors keep the `obj.field` syntax, so clients may not need to change for reads.
- Frameworks that bind to fields by reflection (ORMs, serializers, form libraries) may need configuration to use accessors or private fields.
- Inverse: exposing the field again. Do so only for true data carriers.

## Related

- Fixes [Data Class](https://aipatterns.dev/refactoring/smells/data-class.md) and [Inappropriate Intimacy](https://aipatterns.dev/refactoring/smells/inappropriate-intimacy.md).
- [Self Encapsulate Field](https://aipatterns.dev/refactoring/techniques/self-encapsulate-field.md) applies the same idea inside the class.
- After encapsulating, use [Move Method](https://aipatterns.dev/refactoring/techniques/move-method.md) to pull client logic into the class.

## Agent checklist

- [ ] The field is private and the compiler confirms no direct access remains.
- [ ] Setters exist only where clients need to write, and prefer intent-named methods.
- [ ] Getters don't leak mutable internals.
- [ ] Reflection-based frameworks still read and write the data correctly.
- [ ] Tests pass.
