Jul 02, 2026

ERP Module Development: How to Build a Custom Module That Actually Fits the System

A practical guide to building custom ERP modules that integrate cleanly, scale properly, and don't create maintenance nightmares. For developers and ERP consultants.

ERP Module Development: How to Build a Custom Module That Actually Fits the System

Most ERP customization problems aren’t caused by bad developers. They’re caused by developers who built something that worked fine in isolation but didn’t account for how the broader system behaves. The module installs cleanly. The feature works. Six months later, an upgrade breaks it, or another module conflicts with it, or the client adds a new requirement and suddenly the whole thing needs to be rewritten.

Building a custom module for an ERP isn’t like building a standalone app. The constraints are different, the failure modes are different, and the things that matter most are often invisible until something goes wrong. This post is about getting that right from the start.

What a Custom Module Actually Is (and What It Isn’t)

There’s a common misconception among developers new to ERP work: a custom module is just a feature you bolt on.

It’s not. A module is a self-contained unit of business functionality that participates in the broader system. It has models, views, permissions, and potentially its own workflows. It can depend on other modules. Other modules can depend on it. It needs to behave correctly whether it’s installed alone or alongside 15 other modules.

That distinction matters because it changes how you design things.

A feature you bolt on is tightly coupled to the specific client’s setup. It works until something changes.

A proper module is designed with boundaries. It knows what it owns, what it borrows from other modules, and what it exposes for others to use. It can be installed, uninstalled, updated, and extended without the whole system needing to hold its breath.

The goal isn’t just to build something that works. It’s to build something that keeps working.

Start With the Data Model, Not the UI

Every time I see a module that causes ongoing maintenance pain, the root cause traces back to a data model that was designed by looking at the form first.

Don’t do that.

The form is a view of the data. The data model is the actual structure of your business logic. If you get the data model wrong, the form will look fine and the logic underneath will be fragile.

Before writing anything, answer these questions:

  • What entities does this module own?
  • What entities does it reference from other modules?
  • What relationships exist between them, and which direction does ownership flow?
  • What fields are required versus optional?
  • What fields will need to be queried frequently?

A solid data model tells you almost everything you need to know about how the module will behave. It tells you where permissions need to apply, where computed fields will live, and where performance bottlenecks are likely to appear.

One practical rule: if a field on your model references an entity from another module, that’s a dependency you need to declare explicitly. Don’t leave it implicit. If the other module isn’t installed, your model needs to handle that gracefully, or your module should declare a hard dependency that prevents installation without it.

This is especially relevant in a modular ERP like Fullfinity, where modules can be installed and uninstalled independently. A module that silently assumes another module’s data is present is a module that will break in unpredictable ways.

Designing for Extension, Not Just for Now

Here’s where most custom module work goes sideways. The developer builds exactly what the client asked for, ships it, and moves on. Then the client’s requirements change. Or another consultant inherits the project and needs to extend the module. And there’s no clean way to do it without modifying the original.

Build extension points in from the start.

This doesn’t mean over-engineering. It means being deliberate about what your module exposes versus what it keeps internal. A few things to think about:

Model inheritance. If you’re building a module that other modules might logically extend, structure your models to support that. Don’t make everything a flat list of fields with no clear hierarchy.

Computed fields versus stored fields. Computed fields are easier to override in extensions. Stored fields are faster to query but harder to change behavior on without touching the original module. Know which you’re using and why.

Workflow hooks. If your module triggers actions when something happens (an order is confirmed, a record is approved), expose that as a hookable event rather than hardcoding the downstream behavior. This is the difference between a module that can be extended and one that has to be forked.

We’ve written before about extending models without monkey-patching. The same principles apply when you’re building a module that others will extend. Design it so extension is possible without modifying your source.

Permissions Aren’t an Afterthought

I’ve seen modules shipped to production with no access control beyond “authenticated user can do everything.” That’s not a security posture. That’s a liability.

Every module needs to define its own access rules clearly. Not at the end of development. During design.

Ask these questions early:

  • Who should be able to create records in this module?
  • Who should be able to read them? All records, or only their own?
  • Who should be able to edit and delete?
  • Are there record-level conditions that change access? (For example, a record that’s been approved shouldn’t be editable by regular users.)

In Fullfinity, access control is built into the ORM layer through the access-aware query system. That’s actually helpful when you’re building a module, because you can define access rules at the model level and they’ll be enforced consistently across the API. You’re not manually adding permission checks everywhere.

But you still have to define the rules. The system enforces what you specify. If you specify nothing, you get nothing.

A good habit: define at least three permission tiers for any significant module. Read-only, standard user, and admin. You can always consolidate later. It’s much harder to add granularity after clients have been using the system for a year.

How Schema Changes Work (and Why You Need to Think About Them)

One of the more painful parts of traditional ERP development is managing database migrations. You add a field, you write a migration file, you test it, you run it in production, you hope it doesn’t conflict with something.

Fullfinity handles schema changes automatically through the ORM, which means you don’t write migration files manually. When you change a model, the system reconciles the schema. We’ve covered this in detail in the zero migration files post.

But automatic schema management doesn’t mean you can be careless about schema changes.

A few things still require careful thought:

Removing fields. The system can drop a column, but if any other module or any external integration is querying that field, dropping it will break them. Always check dependencies before removing anything.

Renaming fields. From the ORM’s perspective, a rename looks like a drop and an add. You’ll lose the data in that column unless you handle the migration explicitly or do it in a way the system understands.

Adding required fields. If you add a required field to an existing model, existing records need a value. Either supply a default or handle the backfill. Don’t assume existing records will be fine.

Changing field types. This is high-risk. Changing a text field to a foreign key, or an integer to a decimal, can break queries, break the UI, and break imports. Plan these carefully and test them thoroughly.

The fact that you don’t have to manage migration files is a genuine productivity win. But understanding what’s happening underneath still matters.

Frontend Integration: Forms and Views That Actually Reflect Your Module

Your module’s UI needs to be defined clearly. Not just “it works” but “it works the way users actually need to work with it.”

In Fullfinity, forms and views are defined in YAML, which means they’re declarative and separate from your business logic. This is a good constraint, not a limitation. It forces you to think about the UI as a specification rather than as code.

A few things that catch module developers off guard:

Field ordering matters more than you think. Users build habits around where fields appear. If your module has a form with 20 fields and they’re in a random order, users will make mistakes. Group related fields. Put required fields near the top. Put rarely-used fields at the bottom or in a secondary section.

List views and form views are different problems. Your list view should show enough to identify a record and see its status at a glance. Your form view is for full detail and editing. Don’t try to cram everything into the list view.

Widgets change the user experience significantly. Fullfinity has 57+ widgets available. A date range field rendered as two text inputs is frustrating. The same field rendered as a date picker is obvious. Know what widgets are available and use the right ones. Check out YAML-based form rendering for more on how this works in practice.

Responsive behavior. If your client’s users are on mobile or tablet, your form layout needs to account for that. A four-column layout that looks fine on a desktop is unusable on a phone.

Test your forms with real users before shipping. Not just developers. Real users who will tell you that a field is in the wrong place or a label is confusing.

Testing a Module Before It Touches a Real System

Testing an ERP module is harder than testing a standalone app because the surface area is large and the dependencies are real.

Here’s a pragmatic testing approach that I’ve found actually holds up:

Unit test your business logic in isolation. Any calculations, validations, or state transitions in your models should be testable without a running database or a full ERP instance. If your logic is too tangled with the database layer to test in isolation, that’s a design problem to fix.

Integration test at the module boundary. Test that your module’s API endpoints return what they should, that permissions are enforced correctly, and that relationships between models work as expected. This catches the majority of real-world bugs.

Test with other modules installed. This is the one developers skip most often, and it’s where conflicts show up. Install your module alongside the other modules your client is using, and run through the main workflows. Look for unexpected behavior: missing fields, broken permissions, queries that are suddenly much slower.

Test schema changes specifically. If you’re updating an existing module, test the schema reconciliation on a copy of the production database before running it in production. What works on an empty database doesn’t always work on a database with three years of data.

One more thing: document your test scenarios. Not just for yourself, but for whoever maintains this module after you. A module with a clear test plan is one a consultant can hand off without anxiety.

Packaging and Handing Off a Custom Module

If you’re a consultant or freelancer, the module you build will eventually be handed to someone else to maintain. Maybe the client’s internal team, maybe another consultant, maybe you six months from now when you’ve forgotten how it works.

Packaging matters.

At a minimum, a properly packaged custom module should include:

  • A clear description of what the module does and what it doesn’t do
  • A list of its dependencies (other modules it requires)
  • Documentation of the data model: what the main entities are, what the key relationships are
  • Documentation of the permission model: who can do what
  • A list of any external integrations or API dependencies
  • Known limitations or edge cases that weren’t handled
  • Instructions for the initial setup if there’s any seeding or configuration required

This isn’t overhead. This is the thing that prevents the 2am call six months later.

We’ve written about client onboarding and instance setup in the context of getting a new instance running quickly. A well-packaged module fits into that process cleanly. A poorly documented module turns every new client deployment into a detective exercise.

Conclusion

Building a custom ERP module that actually works well long-term comes down to a few things that aren’t glamorous but matter enormously.

Design the data model first. Don’t let the UI drive the structure. Get the model right and the rest follows.

Build extension points in deliberately. You will not anticipate every future requirement. But you can build a module that’s extendable without being forked.

Define permissions during design, not after. Access control is part of the feature, not a layer you add at the end.

Test with real data and real module combinations. An isolated test environment will lie to you. The production environment won’t.

Package it like someone else will maintain it. Because they will.

Fullfinity’s modular architecture gives you the infrastructure to do this right: automatic schema management, access-aware querying, YAML-based views, and a proper extension model. But the architecture is only as good as the decisions you make on top of it.

If you’re building a custom module and want to see how Fullfinity’s platform supports this kind of development, explore the platform. Or if you’re already building and want to compare notes, the blog has more on the specifics.

More articles

View all →