In our last lesson, we established a clear understanding of Solidity's data types, distinguishing between value types that are copied and reference types that are pointed to. This laid the groundwork for defining the data our smart contracts will manage. Now, we shift our focus from the data itself to controlling who can interact with it.
This lesson introduces visibility specifiers—the keywords public, private, internal, and external. These are fundamental tools for defining the public interface of your contract while protecting its internal state and logic. For someone with your background in data warehousing and business intelligence, this is analogous to setting permissions on database objects: you expose certain views or APIs to end-users (like a PowerBI report) while keeping the underlying tables and ETL logic restricted to authorized processes and administrators. Mastering visibility is a crucial step toward writing secure and well-designed smart contracts.
By the end of this lesson, you will be able to effectively use all four visibility specifiers to control access to your contract's functions and state variables, a key skill for building the robust tokenization systems you're aiming for.
1. An Overview of Visibility
In Solidity, visibility specifiers determine whether a function or state variable can be called or accessed from within the contract, from derived contracts (through inheritance), or from the outside world (by users or other contracts).
Choosing the right specifier follows the principle of least privilege: you should grant the minimum level of access necessary for a component to do its job. This minimizes the contract's attack surface and leads to clearer, more maintainable code.
The following image provides a high-level summary of the four specifiers, which we will explore in detail.

To see these concepts in action, the following video provides a practical demonstration using Remix, a web-based IDE. The presenter clearly illustrates how each specifier behaves when called from different contexts.
Solidity Tutorial | Visibility Modifiers - Public, External, Internal Private
This tutorial from EdRoh walks through each visibility modifier with clear code examples.
As you watch, focus on how the accessibility of functions changes based on their specifier: Introduction: Watch the overview from the beginning to understand the purpose of visibility and the "principle of least privilege". Pay close attention to the crucial point made around 1:32: private only restricts programmatic access from other contracts; all data on a public blockchain is still visible to external observers. External vs. Public: See the demonstration of how public functions can be called internally, but external functions cannot. This is a key difference. Watch from this segment. Internal vs. Private: This part explains the core difference related to inheritance. Observe how a derived contract can access an internal function from its parent but not a private one. Watch from this demonstration.
2. State Variable Visibility
Visibility specifiers also apply to state variables. By default, state variables have internal visibility.
A key feature is that when you declare a state variable as public, the Solidity compiler automatically generates a "getter" function for it. This allows anyone to read the value of that variable without you having to write an explicit get...() function.
For example, in our SimpleToken contract from the first lesson, declaring balanceOf as public:mapping(address => uint256) public balanceOf;
...automatically created a balanceOf(address) function that anyone could call to check a user's balance.
If you declare a state variable as private, there is no automatic getter, and its value can only be read or modified by code within the same contract.
3. Key Comparisons: Choosing the Right Tool
As the video demonstrated, the most important distinctions are between public/external and internal/private. Let's formalize these comparisons.
Internal vs. Private: The Role of Inheritance
This choice is all about contract architecture and extensibility.
private: Use this for sensitive data or helper functions that are exclusively for the internal workings of a single contract. They are not accessible by any other contract, including child contracts that inherit from it. This ensures complete encapsulation.internal: Use this when you are designing a "base" contract with functionality that you want child contracts to be able to use or extend. This is a cornerstone of building modular and reusable code, a pattern heavily used in frameworks like OpenZeppelin.
The following reading summarizes this distinction clearly.
A Deep Dive into Solidity Visibility Specifiers: Public, ...
This section of the article by Rajesh provides a focused comparison of internal and private.
Please read the section titled Comparing Internal and Private. Focus on how inheritance is the key differentiator between the two.
Public vs. External: Gas Optimization and Intent
This choice is about optimizing for gas and clearly stating a function's intended use.
public: The most flexible option, allowing calls from anywhere. However, this flexibility comes at a small gas cost. When apublicfunction is called externally, its arguments are copied fromcalldata(cheap, read-only transaction data) tomemory(more expensive, temporary storage).external: More restrictive and gas-efficient.externalfunctions can only be called from outside the contract. Their arguments can be read directly fromcalldata, avoiding the costly copy tomemory. You should preferexternaloverpublicfor any function that is only meant to be part of the contract's external API.
The image below illustrates this gas difference with a concrete example.

This next reading provides further details on their use cases.
A Deep Dive into Solidity Visibility Specifiers: Public, ...
This part of the same article contrasts public and external.
Now, read the section Comparing Public and External. This reinforces the trade-off between flexibility and gas efficiency.
4. Visibility in Practice: The OpenZeppelin Pattern
These concepts become very practical when building tokens. The industry-standard OpenZeppelin contracts provide a perfect example of how visibility is used to create secure, extensible tokens.
The common pattern is:
- Core logic (like creating or destroying tokens) is implemented in
internalfunctions, often prefixed with an underscore (e.g.,_mint(),_burn()). - Your contract inherits from the OpenZeppelin base contract.
- You then expose this functionality by creating your own
publicorexternalfunctions (e.g.,mint(),burn()). These functions act as wrappers around theinternalfunctions.
This design is powerful because it allows you to add your own access control and validation logic in the public-facing function before calling the trusted, core internal logic. This is exactly how you will implement roles for minting stablecoins or burning security tokens.
This article shows how OpenZeppelin uses internal functions for its ERC-20 implementation and how you can build on top of it.
Read through the following sections to see this pattern: Under Minting Tokens, note how it describes the _mint function as internal. Similarly, under Burning Tokens, see the same description for the _burn function. Finally, look at the code example under Ownership and Access Control. Notice how the public function mint(...) contains the access control logic (onlyRole(MINTER_ROLE)) and then calls the internal _mint function. This is the pattern you will use frequently.
Conclusion
In this lesson, you've learned how to control the "surface area" of your smart contracts using visibility specifiers. This is a critical aspect of security and design.
Here are the key takeaways:
public: Accessible from anywhere (internally and externally).external: Only accessible from outside the contract. More gas-efficient for external calls thanpublic.internal: Accessible within the contract and by derived contracts. Ideal for creating extensible contract frameworks.private: Only accessible within the contract in which it is defined. Ensures encapsulation but does not hide data from off-chain observers.- Always apply the principle of least privilege, defaulting to the most restrictive specifier (
privateorinternal) and only opening up access when necessary.
You now understand how to define your contract's data types and control who can access its functions. The next logical step is to define the logic inside those functions. In our next lesson, we will cover how to implement conditional logic with if/else and require statements, and how to use loops, always with a careful eye on the gas cost implications of our code.