Custom Joomla 4/5 Modules from Scratch – From Manifest to Caching
We develop custom Joomla modules from scratch. Recently, we faced a task: display a "bestsellers" block on the homepage of an e-commerce store with category filtering and caching. Off-the-shelf solutions didn't work—they pulled unnecessary styles and generated N+1 queries. We wrote our own module in two days. Here's how we do it.
A custom Joomla module is not just a widget. It is a fully controllable element in its template position. It can display data from any tables, external APIs, or compute values on the fly. Examples: latest news, contact form, visitor stats, currency exchange rates, product recommendations. The key is it doesn't slow down the site or conflict with other extensions. As stated in the Joomla module development guide, custom modules are the foundation for flexible architecture.
In this article, we'll walk through a real example: a module that displays featured catalog products. We'll show the structure, manifest, helper with caching, dispatcher, and template. And most importantly—how to avoid common mistakes: duplicate queries, lack of caching, incompatibility with Joomla 5.
Why a custom Joomla module is better than a ready-made component?
Ready-made modules from the marketplace are often overloaded with options you don't need for your project. They can conflict with other extensions, require additional libraries, and slow down the page. A custom module contains exactly the code needed for the task. You control every SQL query, every template, and every setting. The result: minimal load time (LCP < 2.5 s) and no extra HTTP requests. For example, in one project we saved the client on hosting costs by optimizing queries and caching.
When do you need a custom Joomla module?
A custom module is justified when standard solutions are insufficient: integration with a third-party API, output from non-standard tables, complex filtering logic, or caching not supported by ready-made modules. It is also indispensable when maximum performance and minimal code size are critical. The development cost is calculated individually, but the investment quickly pays off through savings in server resources and maintenance time.
How to implement caching in a Joomla module?
Caching is key for performance. In our helper, we use the built-in Cache API of Joomla: we wrap the database query in a cache with a 15-minute time-to-live. This reduces server load and speeds up page loading for the user.
Module Structure
A standard custom module includes the following files:
-
mod_product_highlights.php— entry point -
mod_product_highlights.xml— manifest -
src/Dispatcher/Dispatcher.php— dispatcher -
src/Helper/ProductHighlightsHelper.php— helper -
tmpl/default.php— template -
language/en-GB/...— language files
Manifest
Example Manifest
<?xml version="1.0" encoding="utf-8"?>
<extension type="module" client="site" version="4.0">
<name>mod_product_highlights</name>
<author>Your Company</author>
<version>1.0.0</version>
<description>Featured catalog products</description>
<namespace path="src">MyCompany\Module\ProductHighlights</namespace>
<files>
<filename module="mod_product_highlights">mod_product_highlights.php</filename>
<folder>src</folder>
<folder>tmpl</folder>
</files>
<config>
<fields name="params">
<fieldset name="basic">
<field name="count" type="number" label="Number of products"
default="4" min="1" max="12" />
<field name="category_id" type="sql"
label="Category"
query="SELECT id AS value, title AS text FROM #__catalog_categories WHERE published = 1"
default="" />
<field name="show_price" type="radio" label="Show price" default="1">
<option value="1">Yes</option>
<option value="0">No</option>
</field>
</fieldset>
</fields>
</config>
</extension>
Helper with Caching
Example Helper with Caching
// src/Helper/ProductHighlightsHelper.php
namespace MyCompany\Module\ProductHighlights\Site\Helper;
use Joomla\CMS\Factory;
class ProductHighlightsHelper {
public static function getProducts(object $params): array {
$cache = Factory::getCache('mod_product_highlights', 'callback');
$cache->setCaching(true);
$cache->setLifeTime(15); // minutes
$cacheId = md5('product_highlights_' . json_encode($params));
return $cache->get([static::class, '_fetchProducts'], [$params], $cacheId) ?: [];
}
public static function _fetchProducts(object $params): array {
$db = Factory::getDbo();
$count = (int) $params->get('count', 4);
$catId = (int) $params->get('category_id', 0);
$query = $db->getQuery(true)
->select(['id', 'title', 'price', 'image', 'alias'])
->from($db->quoteName('#__catalog_products'))
->where($db->quoteName('published') . ' = 1')
->where($db->quoteName('featured') . ' = 1');
if ($catId) {
$query->where($db->quoteName('category_id') . ' = ' . $catId);
}
$query->order('ordering ASC')->setLimit($count);
$db->setQuery($query);
return $db->loadObjectList() ?: [];
}
}
Dispatcher
// src/Dispatcher/Dispatcher.php
namespace MyCompany\Module\ProductHighlights\Site\Dispatcher;
use Joomla\CMS\Dispatcher\AbstractModuleDispatcher;
use MyCompany\Module\ProductHighlights\Site\Helper\ProductHighlightsHelper;
class Dispatcher extends AbstractModuleDispatcher {
protected function getLayoutData(): array {
$data = parent::getLayoutData();
$products = ProductHighlightsHelper::getProducts($data['params']);
$data['products'] = $products;
$data['showPrice'] = (bool) $data['params']->get('show_price', true);
return $data;
}
}
Template
// tmpl/default.php
defined('_JEXEC') or die;
use Joomla\CMS\Router\Route;
?>
<div class="mod-product-highlights">
<?php foreach ($products as $product) : ?>
<div class="product-card">
<?php if ($product->image) : ?>
<img src="<?php echo $product->image; ?>"
alt="<?php echo htmlspecialchars($product->title); ?>"
loading="lazy">
<?php endif; ?>
<h4 class="product-card__title">
<a href="<?php echo Route::_('index.php?option=com_catalog&view=product&alias=' . $product->alias); ?>">
<?php echo htmlspecialchars($product->title); ?>
</a>
</h4>
<?php if ($showPrice && $product->price) : ?>
<p class="product-card__price"><?php echo number_format($product->price, 0, '.', ' '); ?> $</p>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
We pay attention to alt attributes: for the product image in the module, we set an alt matching the product name. This improves accessibility and SEO.
Work Process
| Stage | Duration |
|---|---|
| Analysis and specification | 1 day |
| Code development | 1–2 days |
| Testing and debugging | 1 day |
| Integration into project | 0.5 day |
| Total | 3–5 days |
Comparison: Custom Module vs. Ready-Made Plugin
| Parameter | Custom Module | Ready-Made Plugin |
|---|---|---|
| Flexibility | Full (any logic) | Limited by settings |
| Size | Only necessary code (~10–20 KB) | Often 1–5 MB with libraries |
| Performance | Optimized for the task | May slow down due to extra queries |
| Dependencies | None | May require other extensions |
| Joomla version support | Adaptable for 4 and 5 | Not always updated |
What's Included
- Ready module with manifest, helper, dispatcher, template, and language files.
- Caching setup to reduce database load.
- Documentation for installation and configuration.
- Consultation on placement in the desired template position.
- Support for 30 days after delivery.
Timeline and Cost
Development of a custom module takes 3 to 5 days depending on complexity. Cost is calculated individually. We guarantee compatibility with Joomla 4 and 5, and no code errors.
We have years of experience in query optimization, caching, and creating user-friendly admin interfaces. Get a preliminary estimate within 1 day — send your task. Order custom Joomla module development and receive ready code with a guarantee.
Contact us to discuss your task — we'll help implement functionality that perfectly fits your project.







