SOLID Principles for App Architecture in Business Central

In this Areopa Academy webinar, Kamil Sacek — Product Development Manager at Navertica and a long-time Microsoft MVP — looks at the SOLID principles from an architectural angle and applies them to how Business Central extensions are designed, split, and connected through dependencies. Where many SOLID discussions live at the level of classes and methods, Kamil deliberately zooms out to the app and module level: how do you decide whether a piece of functionality belongs in one extension or in two, and which way should the dependency point?

Why Architecture, and Why Now?

Kamil opens with a provocative question: which is better — software that works perfectly but is impossible to change, or software that doesn’t work but is easy to change? Anyone who has touched an old NAV installation recognises the first category. From a developer’s perspective, the second option is usually preferable, because once it’s easy to change, getting it to work is a tractable problem. Continually solving the latter is the whole point of caring about architecture.

He grounds this in Grady Booch’s definition of architecture as “the significant design decisions that shape a system, where significant is measured by cost of change.” Good architectural decisions keep the cost of change roughly constant over time; bad ones cause it to rise. Architecture is important in the Eisenhower-matrix sense, while features are urgent. Both matter, but it is easy to let urgency crowd out importance.

<img src="

Slide: What is Architecture? Architecture decisions measured by cost of change.
” alt=”Slide: What is Architecture? Architecture decisions measured by cost of change.” />

One more framing point before SOLID itself: architecture is the responsibility of the whole team, not only the architect. If only the architect thinks about it, developers will quietly undo those decisions in the implementation.

The Five SOLID Principles

SOLID is the set of principles collected by Robert C. Martin (Uncle Bob) in Clean Architecture:

  • S — Single Responsibility Principle (SRP)
  • O — Open-Closed Principle (OCP)
  • L — Liskov Substitution Principle (LSP)
  • I — Interface Segregation Principle (ISP)
  • D — Dependency Inversion Principle (DIP)

<img src="

Slide listing the five SOLID principles, attributed to Robert C. Martin.
” alt=”Slide listing the five SOLID principles, attributed to Robert C. Martin.” />

Single Responsibility Principle (SRP)

A module should be responsible to one, and only one, actor or stakeholder. Kamil’s everyday picture: a fork does one thing, a spoon does another, and a spork tries to do both. The spork is legal, it works — but the cost of changing either side is higher than changing the simpler tools separately.

<img src="

Slide: SRP illustrated with a fork, spoon, and combined 'spork'.
” alt=”Slide: SRP illustrated with a fork, spoon, and combined ‘spork’.” />

What is a “module” in this context? In Clean Architecture it is a unit of deployment — something with its own version that can be deployed separately. In Business Central, that maps cleanly to an extension. The same principle applies at smaller scales too: a function should also do one thing.

The textbook example of SRP being broken in Business Central is the reservation table — a single table serving many actors with conflicting needs. Another familiar one: a report originally built for the finance department that the sales team starts asking to extend. Each side requests changes in conflict with the other, and you end up in a never-ending change loop. The fix is to give each object exactly one owner.

Practical checks Kamil suggests:

  • Ask “what is the responsibility of this object/function?” If you struggle to name it, the responsibility is probably not single.
  • Add an actor or stakeholder field to user stories so conflicting changes surface early.
  • Don’t create multi-purpose objects just to save object IDs — that constraint is largely gone in the online world.

Open-Closed Principle (OCP)

“Software artifacts should be open for extension but closed for modification.” In Business Central this is the foundational platform behaviour: you cannot edit Microsoft’s Base Application directly, but you can extend it. Interfaces and events are the two main tools the platform gives you to do so.

<img src="

Slide: OCP illustrated with a mixer and interchangeable attachments.
” alt=”Slide: OCP illustrated with a mixer and interchangeable attachments.” />

The mixer analogy is the picture: a single mixer body with multiple attachments. You add a new attachment without ever changing the mixer’s motor. Kamil’s architectural rule that follows from OCP: separate functionality based on how, why, and when it changes, and organise the separated pieces into a hierarchy. High-level business rules should not depend on low-level peripheral concerns. Our apps depend on Base App, not the other way round, because Base App contains the higher-level business rules.

📖 Docs: Extensibility overview – Microsoft Learn — the official catalogue of the AL extension mechanisms (table, page, report, enum, permission set, and event-based extensions) that make OCP achievable in Business Central.

Liskov Substitution Principle (LSP)

Introduced by Barbara Liskov in 1988, LSP says you should be able to replace one part of a system with another implementation without changing the depending part and without changing behaviour. Microsoft’s ongoing refactoring of modules such as Number Series is, in part, about making the Base App’s pieces substitutable in this sense.

The classic AL anti-pattern Kamil shows is a case on a type:

case Type of
    Type::"G/L Account":
        PostGLAccICLine(SalesHeader, SalesLine, ICGenJnlLineNo);
    Type::Item:
        PostItemLine(SalesHeader, SalesLine, TempDropShptPostBuffer, TempPostedATOLink);
    Type::Resource:
        PostResJnlLine(SalesHeader, SalesLine, JobTaskSalesLine);
    Type::"Charge (Item)":
        PostItemChargeLine(SalesHeader, SalesLine);
end;

Adding a new sales line type means hunting down every such case across the codebase. The substitutable alternative is a one-line interface call:

Type.Post(...);

Each line type implements its own Post, and the caller no longer needs to know which one it is dealing with.

<img src="

Slide: LSP, AL case statement on sales line type vs single interface call Type.Post().
” alt=”Slide: LSP, AL case statement on sales line type vs single interface call Type.Post().” />

Kamil’s other heuristic for designing substitutable code: write the process in terms of something rather than a concrete type. Designing a sales process around an unknown “something to be sold” rather than around Item makes it easier to slot in G/L accounts, resources, or new types later.

📖 Docs: Interfaces in AL – Microsoft Learn — the AL interface keyword, implements on codeunits, and how interface variables enable polymorphic method calls. This is the primary tool for honouring LSP in AL.

Interface Segregation Principle (ISP)

“Clients should not be forced to depend upon interfaces that they don’t use.” Kamil applies this at the app level. Suppose your app extends an AppSource warehousing app, and a small part of it also needs Czech localization functionality. The naive design has My App depending on both AppSource App and CSY Localization.

<img src="

Slide: ISP, My App depending on both AppSource app and CSY Localization.
” alt=”Slide: ISP, My App depending on both AppSource app and CSY Localization.” />

The result: every customer of My App drags in the Czech localization, even those who do not need it, and the app no longer works as a W1 product. The ISP-friendly version extracts the localization-specific behaviour into a small companion app — My App CSY — that depends on both My App and CSY Localization. My App itself stays clean.

<img src="

Slide: ISP, splitting My App and a My App CSY companion app.
” alt=”Slide: ISP, splitting My App and a My App CSY companion app.” />

The same logic applies to interfaces themselves: if you are tempted to add a method that only a few implementers need, that’s a hint to create a second, smaller interface rather than forcing every implementer to provide an empty stub.

📖 Docs: App.json file reference – Microsoft Learn — the dependencies and application properties are exactly the surface area where ISP decisions at the extension level get made.

Dependency Inversion Principle (DIP)

Kamil treats DIP as the tool that makes the other principles practical. Normally, if A calls B, then A depends on B. DIP flips that direction by introducing an abstraction in the middle so that the flow of control and the direction of dependency point opposite ways.

His everyday picture: a power socket. The socket is an abstraction — it knows nothing about your plug, and nothing about whether copper or aluminium wires sit behind it. Both the plug and the wiring depend on the socket’s shape, not the other way around.

<img src="

Slide: DIP illustrated with a power socket as abstraction, with plug and wires depending on it.
” alt=”Slide: DIP illustrated with a power socket as abstraction, with plug and wires depending on it.” />

In AL, the two main tools for inverting dependencies are events and interfaces. Base App can effectively call into your app — by raising an event you subscribe to, or by invoking your codeunit through an interface — while your app remains the dependent party in the manifest. Base App never has to know your app exists.

📖 Docs: Events in AL – Microsoft Learn — how publishers, subscribers, and integration/business/trigger events let high-level code invoke low-level code without depending on it. The mechanical foundation of DIP in AL.

Putting It Together: Refactoring a Monolith

In the final stretch, Kamil walks through a worked refactoring. The starting point is a single Base App for the solution that depends on both an AppSource app and the Czech localization. He then applies the principles in order:

  1. ISP + DIP: pull the Czech-specific code out into a Base CSY App that depends on Base App and on CSY Localization. Base App is now W1-clean.
  2. DIP for the AppSource app: introduce a Connector App that depends on the AppSource app and on Base App’s abstractions, so Base App no longer needs a direct AppSource dependency.
  3. SRP: extract reporting and APIs into separate Reporting App and API App packages — they are peripheral concerns, not core business rules.
  4. LSP + ISP: introduce an Abstract App that defines the interface Base App talks to, so the AppSource side can be swapped out by writing a different connector.

<img src="

Slide: dependency graph after refactoring with Abstract App, Connector App, Base CSY App, API App, Reporting App.
” alt=”Slide: dependency graph after refactoring with Abstract App, Connector App, Base CSY App, API App, Reporting App.” />

Every arrow in the final picture points from specific to abstract, from low-level to high-level, from unstable to stable — and crucially, there are no cycles. This is the SaLi (Single-and-Light) architecture that Kamil’s team at Navertica uses across more than a thousand customer extensions. Kamil’s blog goes into more depth on the pattern.

Wrong or Correct?

Kamil ends with a discussion question: Base App from Microsoft depends on System App. Following the SOLID rules strictly, System App looks like a peripheral concern (system services) and Base App holds the business rules — so shouldn’t the dependency point the other way?

<img src="

Slide: Wrong or correct? Base App depending on System App.
” alt=”Slide: Wrong or correct? Base App depending on System App.” />

His own answer: it would be cleaner to insert an abstract System Interface App in the middle, and have both Base App and System App depend on it. In practice System App already separates its public interfaces from its internal implementation at the object level, so the spirit of the principle is preserved even if the package boundaries don’t reflect it perfectly.

Q&A Highlight: The Shared Report Problem

In the Q&A, John Long asked a sharp question: if warehouse asks for fields ABC and finance asks for XYZ, and you build two extensions, both groups still see all six fields. Isn’t that the spork all over again?

Kamil’s answer: the object can technically carry all six fields, but only one department must be the formal owner and the single point of contact for change. The other department has to negotiate through them. The SRP isn’t about restricting who sees the object, it’s about pinning down who can change it. That ownership needs to be written down somewhere your consultants can find it — otherwise the back-and-forth starts again.

Key Takeaways

  • Architecture is decided by cost of change. Track that explicitly when making design decisions.
  • Each module (extension, codeunit, function) should have exactly one actor responsible for it.
  • Extend by adding new pieces; don’t change existing ones. Interfaces and events are the platform-blessed tools.
  • A long case on a type is a signal that an interface is missing.
  • Don’t pollute an app with dependencies a few code paths need — split out a companion app instead.
  • Use events and interfaces to invert dependency direction so high-level, stable apps don’t depend on low-level, unstable ones.

This post was drafted with AI assistance based on the webinar transcript and video content.