A codeunit is a container for procedures and nothing stops you putting everything in one. The base application has a few conventions that keep its thousands of codeunits navigable, and they apply equally to a per-tenant extension. How to decide what goes in which codeunit, the facade pattern the System Application uses, local versus internal versus public, and the three codeunits every extension has.
Codeunit design is where an extension is either readable in a year or not. The platform does not care; the next developer does. These are the conventions that make the base application navigable and that we apply to every extension.
A codeunit is named for what it does to what: "CON Courier Shipment Mgt." manages shipments (create, release, cancel); "CON Courier API Client" talks HTTP; "CON Courier Label Printer" renders labels. When a codeunit has procedures about two different things, it is two codeunits. When a procedure is three hundred lines, it is several procedures. The test: can you say in one sentence what this codeunit is responsible for, without "and"?
The base application's "Sales Header Mgt.", "Item Tracking Management", "DimensionManagement": a management codeunit is the business operations on an entity, the verbs a user or another codeunit calls (Release, Reopen, Archive, Copy). It is the public face of the entity. It is not a dumping ground for utilities, and it does not do HTTP, file handling or UI; those are their own codeunits that Mgt. calls.
The System Application exposes each module through one public codeunit (the facade: Email, "Temp Blob", "No. Series") and keeps the implementation in Access = Internal codeunits behind it. Callers only see the facade; Microsoft can rewrite the implementation without breaking anyone. Do the same for any area of your extension that other apps, or other parts of your own app, call: one public codeunit with a small, documented surface, and everything else internal.
codeunit 50100 "CON Courier" // facade, Access = Public
{
procedure Send(var Shipment: Record "CON Courier Shipment")
var
Impl: Codeunit "CON Courier Impl.";
begin
Impl.Send(Shipment);
end;
}
codeunit 50101 "CON Courier Impl." // Access = Internal
{
internal procedure Send(var Shipment: Record "CON Courier Shipment")
...
}
local: callable only within the codeunit. The default for everything.internal: callable within the app (and apps in internalsVisibleTo). For implementation shared across your codeunits.[Obsolete] before you change it.Subtype = Install): first-time setup, registering upgrade tags, default data, retention policy registration.Subtype = Upgrade): data moves between versions, guarded by tags (upgrade post).SingleInstance = true makes a codeunit's global variables live for the session. Use it for caches and flags (the "GetRecordOnce" pattern, a "suppress event during import" flag). Do not use it for state that should be per operation; it leaks between calls in the same session.
"CON Utilities" or "CON Helpers" with forty unrelated procedures.