Popup YouTube Video
Sheerpower Logo

Large Application Architecture


Learn how to organize a real 50,000+ line Sheerpower application using %include files, modules, private routines, scoped routines, local routines, and a clear application structure.

Best Practice: Smart File Paths with `@`
To keep your code portable and self-contained, use the @ symbol in file paths. The `@` is a special character that always refers to the directory where your program is currently running.

Example:
%include "@utilities.spinc"

No matter where you move your program, @utilities.spinc will always point to utilities.spinc in the same folder as your running program. This ensures your code finds its files reliably, without hardcoded paths.

Large Application Architecture

Small programs can live in one source file. Large business applications cannot. Once an application grows past a few thousand lines, the main challenge is no longer writing individual statements. The challenge is organizing the program so it remains understandable, testable, and safe to change.

Sheerpower gives you the building blocks for this: %include files, modules, private routines, scoped routines, local routines, and clear routine boundaries. This tutorial shows how to put those pieces together into a practical architecture for a real 50,000+ line application.

Problem: A large application that starts as one file often becomes difficult to understand. Every change feels risky because data access, business rules, display logic, and helper routines are all mixed together.

Solution: Divide the application into clear source files and modules. Use %include to assemble the application, use routines to create stable boundaries, and keep internal helper logic private whenever possible.

Efficiency: A well-organized Sheerpower application is easier for both humans and AI tools to modify. Each file has a clear job, and the compiler can still build the full application quickly.

Takeaway: Large Sheerpower applications should be organized around responsibility, not just around line count.

The Goal of Application Architecture

Application architecture is the plan for where things belong. It answers questions like:

  • Where does the program start?
  • Where are constants and configuration values defined?
  • Where are tables opened and accessed?
  • Where are business rules kept?
  • Where are reports, screens, or web handlers kept?
  • Which routines are public, and which are internal helpers?

The purpose is not to make the program look complicated. The purpose is to make the program easy to reason about when it becomes large.

A Practical Folder Layout

A 50,000+ line application usually benefits from a simple folder layout. The exact names are not important. What matters is that the structure is predictable.

c:\projects\my_app\source\ main.spsrc app_config.spinc app_startup.spinc constants.spinc table_defs.spinc common_types.spinc customer_rules.spinc invoice_rules.spinc payment_rules.spinc customer_report.spinc invoice_report.spinc menu_handlers.spinc screen_handlers.spinc string_utils.spinc date_utils.spinc format_utils.spinc

The Main Program Should Be a Conductor

In a small program, the main source file may do everything. In a large application, the main program should act more like a conductor. It should assemble the application, initialize it, and hand control to the proper routines.

program invoice_system %include "@app_config.spinc" %include "@constants.spinc" %include "@includes\table_defs.spsrc" %include "@customer_rules.spinc" %include "@invoice_rules.spinc" %include "@payment_rules.spinc" %include "@customer_report.spinc" %include "@invoice_report.spinc" %include "@menu_handlers.spinc" %include "@screen_handlers.spinc" %include "@string_utils.spinc" %include "@date_utils.spinc" %include "@format_utils.spinc" do_startup do_main_menu do_shutdown end

Notice what the main program does not contain. It does not contain detailed invoice calculations. It does not contain report formatting. It does not contain low-level table lookup code. Those details live in the appropriate modules.

Problem: When the main source file contains all of the application logic, every part of the program becomes connected to every other part.

Solution: Keep the main program short. Let it include the required modules, initialize the application, call the main entry point, and then shut down cleanly.

Efficiency: Developers can find code faster because the main program describes the application structure without hiding details inside a giant source file.

Takeaway: The main program should explain the shape of the application, not contain the whole application.

Use %include to Assemble the Application

The %include directive lets one source file include another source file. This allows a large application to be written as a collection of smaller, focused files while still compiling as one application.

A good include file has a clear purpose. For example:

  • invoice_rules.spinc handles invoice business rules.
  • format_utils.spinc handles reusable formatting.
  • invoice_report.spinc handles invoice report output.

Avoid creating include files that are vague catch-all containers. A file named misc.spinc will eventually become a junk drawer. A large application needs names that tell the next developer where to look.

Recommended Include Order

Include order should be deliberate. A simple pattern is:

  • Configuration first.
  • Constants and shared definitions next.
  • Data access modules next.
  • Business rule modules next.
  • Reports, screens, web handlers, or menus next.
  • General utility routines last or near the end.
// 1. Configuration %include "app_config.spinc" // 2. Shared definitions %include "@constants.spinc" %include "@table_defs.spinc" // 3. Data access %include "@customer_data.spinc" %include "@invoice_data.spinc" // 4. Business rules %include "@customer_rules.spinc" %include "@invoice_rules.spinc" // 5. User-facing features %include "@invoice_report.spinc" %include "@menu_handlers.spsrc" // 6. Utilities %include "@format_utils.spinc"

The goal is to make the dependency direction obvious. Business rules may use data access routines. Reports may use business rule routines. Utility routines should be general and should not depend on application-specific screens or reports.

Think in Layers

A large Sheerpower application is easiest to understand when it is organized in layers.

User interface layer menus, screens, web handlers, command handlers Reporting layer printed reports, exports, summaries Business rule layer validation, calculations, approval rules, workflow rules Data access layer table access, lookup routines, save routines, find routines Shared utility layer formatting, string helpers, date helpers, simple reusable tools

Layers do not need to be complicated. They are simply a discipline: code that talks to the user should not also contain low-level table lookup details. Code that calculates invoice totals should not also format the report header.

Data Access Modules

Data access modules should hide the details of finding, reading, validating, and saving records. The rest of the application should ask for what it needs through clearly named routines.

// @customer_data.spinc routine customer_exists with cust_id$, returning exists? exists? = false // Lookup customer record here. // Set exists? to true if the customer is found. end routine routine get_customer_name with cust_id$, returning name$ name$ = "" // Lookup customer record here. // Return the customer name. end routine

With this pattern, the rest of the application does not need to know how the customer table is searched. It only needs to call the routine that answers the question.

Business Rule Modules

Business rules should describe what the application means, not how the screen works or how the report is printed.

// invoice_rules.spinc routine invoice_can_be_posted with invoice_id$, returning ok? ok? = false // Check whether the invoice exists. // Check whether the customer exists. // Check whether the invoice has detail lines. // Check whether the invoice has already been posted. end routine routine calculate_invoice_total with invoice_id$, returning total total = 0 // Read invoice detail lines. // Add taxable and non-taxable amounts. // Apply discounts and taxes. end routine

This keeps the meaning of the application in one place. If the rule for posting an invoice changes, the change belongs in the business rule module, not scattered across menus, reports, and data entry screens.

Problem: Business rules often get copied into multiple places: once in the screen, once in the report, once in the import program, and once in the batch job.

Solution: Put the rule in one routine with a clear name. Then call that routine wherever the rule is needed.

Efficiency: When the rule changes, one routine changes. The rest of the application automatically uses the corrected logic.

Takeaway: A business rule should have one home.

User Interface Modules

User interface modules should handle interaction. They should ask questions, display menus, collect input, and call business routines. They should not contain the deep business rules themselves.

// menu_handlers.spinc routine do_main_menu do print print "1. Customers" print "2. Invoices" print "3. Reports" print "4. Exit" line input "Choice": choice$ select case choice$ case "1" do_customer_menu case "2" do_invoice_menu case "3" do_report_menu case "4" exit do case else print "Please choose 1, 2, 3, or 4." end select loop end routine

The menu routine controls the conversation with the user. It does not calculate invoice totals, validate posting rules, or know the physical structure of the customer table.

Report Modules

Report modules should focus on report output. They may call data access routines and business rule routines, but the report itself should not become the only place where important calculations exist.

// invoice_report.spsrc routine print_invoice_summary customer_name$ = customer->name$ total = customer->total print "Invoice Summary" print "Customer: "; customer_name$ print "Total: "; total end routine

This keeps the report readable. The report describes what is being printed. The calculation remains in the business rule module.

Utility Modules

Utility modules should contain small, reusable helpers that are not tied to one screen, report, or business object.

// format_utils.spinc routine calc_money with amount, returning text$ text$ = sprintf$("%m", amount) end routine routine yes_no with flag?, returning text$ if flag? then text$ = "Yes" else text$ = "No" end if end routine

Utility routines should stay general. A routine named calc_money can be used anywhere. A routine named format_invoice_customer_warning probably belongs in an invoice module, not in a general utility file.

Public Routines and Private Helpers

A large application should expose a small number of clear routines and hide the helper routines that are only used internally.

Think of each module as having two parts:

  • The public routines other modules are expected to call.
  • The private helpers used only inside that module.
// invoice_rules.spinc routine calculate_invoice_total with id, returning total get_invoice_subtotal with id ce_id$, returning subtotal calculate_invoice_tax with id invoice_id$, subtotal, returning tax total = subtotal + tax end routine private routine get_invoice_subtotal with id, returning subtotal subtotal = 0 // Internal helper used by this module. end routine private routine calculate_invoice_tax with id, subtotal, returning tax tax = 0 // Internal helper used by this module. end routine

The important routine is calculate_invoice_total. Other modules should call that routine. The subtotal and tax helpers are implementation details.

Problem: When every routine is visible and callable from everywhere, the application slowly loses structure. Other modules begin depending on internal helper routines that were never meant to be public.

Solution: Keep helper routines private when they are only needed inside one module. Expose only the routines that represent the module's intended interface.

Efficiency: Private helpers reduce accidental coupling. The module can be improved internally without breaking the rest of the application.

Takeaway: Public routines are promises. Private routines are implementation details.

Use Local Routines for One-Time Internal Logic

Some helper logic is so specific that it should not be visible even as a normal module helper. In those cases, a local routine can keep the helper close to the routine that uses it.

routine print_customer_activity print "Customer Activity" local print_line with label "Customer", value customer->cust_id$ local print_line with label "Status", value customer->status$ end routine local routine print_line with label, value print label$; ": "; value$ end routine

A local routine is useful when the helper has no meaning outside the enclosing routine. This keeps the module cleaner and prevents the helper name from becoming part of the larger application vocabulary.

Use Scoped Routines to Control Visibility

Scoped routines help a large program keep names under control. In a large application, routine names can multiply quickly. Scoping helps prevent unrelated parts of the program from accidentally depending on each other.

The architectural idea is simple:

  • Use broad visibility only for routines that are true interfaces.
  • Use private visibility for module-only helpers.
  • Use local routines for helpers used by only one routine.

This creates a natural ladder of visibility:

Application-level routine Can be called by other major parts of the application. Private module helper Used only inside one source file or module. Local routine Used only inside one enclosing routine.

As a rule, choose the smallest visibility that works.

Naming Routines in Large Applications

Names matter more as programs get larger. A 200-line program can survive vague names. A 50,000-line application cannot.

Good routine names usually say what question they answer or what action they perform.

customer_exists get_customer_name invoice_can_be_posted calculate_invoice_total print_invoice_summary load_application_settings validate_payment_amount

Avoid names that only describe implementation details or are too general to be useful.

process_data do_stuff handle_record run_logic check_it misc_calc

In a large application, the name of a routine is documentation. A clear name reduces the need to open the routine just to understand why it is being called.

Separate Policy from Mechanics

Large applications often mix two different kinds of code:

  • Policy: What the business rule is.
  • Mechanics: How the program reads, writes, prints, or displays something.

For example, "an invoice cannot be posted until it has at least one detail line" is policy. "read the invoice detail table and count the lines" is mechanics.

Keep policy visible in business rule routines. Hide mechanics in data access helpers.

routine invoice_can_be_posted returning ok? ok? = false if not invoice_exists? then exit routine if not invoice_has_detail_lines? then exit routine if invoice_already_posted? then exit routine ok? = true end routine

This routine reads like the business rule itself. The details of how each question is answered are hidden behind clearly named routines.

A 50,000+ Line Application Skeleton

The following skeleton shows how a larger application can be divided. The exact number of lines is not important. The important point is that each file has a clear purpose.

main.spsrc 100 lines app_config.spinc 150 lines constants.spinc 200 lines table_defs.spinc 300 lines customer_data.spinc 450 lines invoice_data.spinc 600 lines payment_data.spinc 400 lines customer_rules.spinc 500 lines invoice_rules.spinc 700 lines payment_rules.spinc 350 lines customer_report.spinc 400 lines invoice_report.spinc 500 lines menu_handlers.spinc 300 lines screen_handlers.spinc 450 lines string_utils.spinc 200 lines date_utils.spinc 150 lines format_utils.spinc 150 lines

This is still one application. It is simply divided into pieces that match how the application is understood.

Design the Application Around Use Cases

A practical way to organize a large application is to list the major things the user needs to do.

  • Create and edit customers.
  • Create invoices.
  • Post invoices.
  • Record payments.
  • Print customer reports.
  • Print invoice summaries.

Then map each use case to the modules it needs.

Post invoice %include "@menu_handlers.spinc" %include "@invoice_rules.spinc"" %include "@invoice_data.spinc" %include "@customer_data.spinc" %include "@invoice_report.spinc"

This makes the application easier to test. When the posting logic is wrong, you know to start with the invoice business rules, not with the report or menu code.

Keep Routine Interfaces Small

A routine interface is the list of information the caller must provide and the value or values the routine returns. In a large application, small interfaces are easier to use correctly.

routine calculate_invoice_total with id invoice_id$ returning total mytotal total = 0 // The caller provides only the invoice ID. // The routine knows how to find the needed detail records. end routine

Compare that to a routine that requires the caller to supply every detail needed for the calculation. That kind of routine is harder to call and easier to call incorrectly.

Problem: Large parameter lists make routines fragile. The caller must understand too many internal details.

Solution: Pass the smallest meaningful input, such as an ID, key, or business object reference. Let the routine gather the details it owns.

Efficiency: Smaller interfaces reduce mistakes and make the application easier to change later.

Takeaway: A good routine hides complexity behind a simple, meaningful interface.

Avoid Circular Dependencies

A circular dependency happens when two modules depend on each other. For example, invoice rules call report routines, and report routines call invoice rules. This makes the application harder to understand because neither module is clearly above the other.

A better direction is:

reports call business rules business rules call data access data access calls table routines

The lower layer should not call back into the higher layer. Data access should not print reports. Business rules should not display menus. Reports should not own core business calculations.

Use Comments to Explain Boundaries

In a large application, comments should explain purpose and boundaries, not repeat every statement.

// invoice_rules.spinc // // This module owns invoice business rules. // It may call invoice_data and customer_data routines. // It should not print reports or display menus.

A short header like this helps future developers understand what belongs in the file and what does not.

Suggested Module Header

A consistent module header makes large applications easier to browse.

// ------------------------------------------------------------ // Module: invoice_rules.spinc // // Purpose: // Owns invoice validation, posting rules, and invoice totals. // // Public routines: // invoice_can_be_posted? // calculate_invoice_total // post_invoice // // Private helpers: // get_invoice_subtotal // calculate_invoice_tax // // Depends on: // invoice_data.spsrc // customer_data.spsrc // ------------------------------------------------------------

This header is not for the compiler. It is for the next human, and for the AI assistant that may help maintain the program later.

Architecture Helps AI Tools

A well-structured Sheerpower application is easier for AI tools to work with. The AI does not need to understand the entire program at once. It can work inside the right module and make a small change with less risk.

For example, if the request is:

Add a rule that invoices over $10,000 require manager approval.

The correct place is probably:

invoice_rules.spsrc

It is probably not:

menu_handlers.spsrc invoice_report.spsrc format_utils.spsrc

Architecture reduces guessing. That helps humans, and it helps AI.

Problem: AI tools can make poor changes when they cannot tell where a feature belongs.

Solution: Organize the application so each file has a clear responsibility. Use names, module headers, and routine boundaries that reveal intent.

Efficiency: The AI can make smaller, safer edits. Fast Sheerpower compiles then allow the change to be checked quickly.

Takeaway: Good architecture turns AI assistance from guessing into guided maintenance.

Checklist for a Large Sheerpower Application

Before an application grows too large, ask these questions:

  • Is the main program short and easy to understand?
  • Are include files grouped by responsibility?
  • Are data access routines separated from business rules?
  • Are reports and screens calling business routines instead of duplicating rules?
  • Are helper routines private when they should be private?
  • Are local routines used for one-routine-only helper logic?
  • Are routine names specific and meaningful?
  • Are module headers explaining purpose and dependencies?
  • Are circular dependencies avoided?
  • Can a new developer quickly find where a change belongs?

Summary

Large Sheerpower applications should be organized around clear responsibilities. Use %include to assemble the application from focused source files. Keep the main program small. Separate data access, business rules, reports, user interface code, and utilities.

Use routine boundaries to make the application easier to understand. Keep public routines clear and stable. Keep helper routines private when possible. Use local routines when the helper belongs to only one enclosing routine.

The result is an application that can grow beyond 50,000 lines while still remaining readable, maintainable, and safe to change.

(Show/Hide Sheerpower Large Application Architecture Takeaways)
Hide Description

    

       


      

Enter or modify the code below, and then click on RUN

Looking for the full power of Sheerpower?
Check out the Sheerpower website. Free to download. Free to use.