Encapsulation controls how valid state can change
Encapsulation keeps a type's state behind an API whose operations can enforce rules. Ledger exposes Balance for reading but gives its setter private accessibility. Callers can observe the balance while changes normally pass through Deposit, where the type applies its positive-amount rule. This is more useful than a public mutable field when several callers need to share one consistent update policy instead of remembering to repeat the same guard everywhere.
Private accessibility is about the containing type, not simply about being in a different file. Code outside Ledger does not gain setter access because it happens to appear in the same Program.cs file. At the same time, this design is not an authorization system for real financial accounts. It describes an in-process API boundary. The teaching model uses integers and a minimal acceptance rule; currency precision, account ownership, persistence, concurrent access, and audit requirements would each need separate decisions in a production domain.
Observe one accepted update and one ignored call
A newly created Ledger starts with the default integer Balance of zero. Calling Deposit with twelve passes amount > 0, so the body adds twelve and stores the result. Calling Deposit with negative three fails the condition, so the update body is skipped. The second call neither subtracts three nor throws an exception. WriteLine reads the remaining balance and prints 12. The outcome follows this specific method's policy rather than a language rule that negative numbers cannot be added.
The setter still executes inside Ledger when its method assigns Balance = Balance + amount. Private does not make Balance unchangeable; it restricts who may perform that assignment directly. A caller can keep another reference to the same Ledger and invoke Deposit through it, but the update still goes through the same guard. Encapsulation therefore controls an operation boundary around shared state. It does not automatically create a snapshot, give each reference an independent balance, or prevent every permitted caller from making a deposit.
public void Deposit(int amount)
{
if (amount > 0)
{
Balance = Balance + amount;
}
}An ignored operation may need a visible result in another API
Deposit returns void, so the caller receives no explicit accepted-or-rejected status. Silent ignoring is the sample's chosen behavior, not a universal recommendation. A user-facing workflow may need a result value or a specific exception to explain rejection. Decide that contract deliberately before changing the code. Also preserve invariants across every future operation: adding a public Withdraw or correction setter without the relevant checks could bypass the careful Deposit rule while leaving the original method unchanged.
Test the boundary, then make rejection explicit
First add Deposit with zero and then Deposit with five after the original calls. Predict a final balance of seventeen: zero fails the positive guard, and five succeeds. Hint: trace the balance after each call rather than summing every argument indiscriminately. In a separate controlled experiment, try assigning Balance from the top-level code and explain the compiler's accessibility complaint instead of treating it as a runtime rejection.
Next create a TryDeposit variant that returns a boolean while preserving the positive-only rule. Predict true for twelve and false for negative three and zero, with the same resulting balance for the accepted inputs. Hint: return failure before changing state. Then design a withdrawal operation with a no-overdraft invariant and test insufficient funds as a distinct rejection. Explain why an operation's validity rule belongs near the mutation instead of being trusted to every caller independently.
Review the entire public mutation surface
List which members expose state and which members can change it. Explain the default balance, the successful update, the skipped negative call, and the final twelve. When extending an encapsulated type, review every new mutating operation rather than only checking that a field is private. The public API should make accepted transitions and rejected requests understandable, while the implementation remains responsible for preserving the same domain rules after each successful operation.
- Balance is publicly readable and privately settable.
- Private does not mean any code in the same file may set it.
- The negative deposit is ignored by this sample’s guard.
- The unchanged program prints one line:
12.
A working example
var account = new Ledger();
account.Deposit(12);
account.Deposit(-3);
Console.WriteLine(account.Balance);
class Ledger
{
public int Balance { get; private set; }
public void Deposit(int amount)
{
if (amount > 0)
{
Balance = Balance + amount;
}
}
}
Reject the amount before you add it.