How to Build a Custom Customer Segmentation Module for Targeted Marketing in Magento 2

Want to build a custom "Customer Segmentation" module for targeted marketing in Magento 2? Nice. In this post I’ll walk you through a practical, step-by-step implementation: module skeleton, database architecture, how to integrate native and custom customer attributes, a flexible rule engine to define segments by purchase behavior or location, a REST API so external marketing tools (Mailchimp, Klaviyo, etc.) can consume segments, and a simple analytics dashboard to measure performance.
Why build a custom segmentation module?
Magento 2 has customer attributes and marketing tools, but a custom segmentation module gives you full control to:
- Create reusable segments using business rules (e.g., "customers who bought X in last 90 days").
- Use native and custom attributes (city, group, custom loyalty score).
- Expose segments via a REST API to feed external tools like Klaviyo or Mailchimp.
- Measure segment performance via an integrated dashboard.
It’s especially useful for stores that want tighter control than a third-party SaaS solution or want to keep segmentation logic close to data to avoid sync issues.
High-level architecture
Let’s design a clear architecture so the code stays maintainable:
- Database entities: segment, rule, rule_condition, segment_customer (materialized membership cache).
- Rule engine: interprets rules and evaluates them against customer data (attributes, order history, location).
- Updater / indexer: re-evaluates segments periodically or on events to update segment_customer table.
- REST API: read-only endpoints to export segments and members.
- Admin UI: create/edit segments and rules, and a dashboard to view segment performance.
Module skeleton
Start with a standard Magento module layout. Example module name: Magefine_CustomerSegment.
<app/code/Magefine/CustomerSegment/registration.php>
<?php
use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(
ComponentRegistrar::MODULE,
'Magefine_CustomerSegment',
__DIR__
);
<app/code/Magefine/CustomerSegment/etc/module.xml>
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
<module name="Magefine_CustomerSegment" setup_version="1.0.0" />
</config>
We will need other usual pieces: di.xml, acl.xml (for admin), routes.xml (adminhtml + webapi), and db_schema.xml to define tables. The next sections show the DB design in more detail.
Database architecture
Designing database tables well keeps things fast and predictable. Use Magento's db_schema.xml (recommended over install/upgrade scripts).
Core tables:
- magefine_segment: stores segment metadata (name, description, active, updated_at, unique code/slug)
- magefine_segment_rule: top-level rules per segment (e.g., combine rule groups)
- magefine_segment_condition: low-level conditions (attribute = city, order_count > 3)
- magefine_segment_customer: materialized table of customer_id to segment_id (cached membership)
Example db_schema.xml snippets (simplified):
<!-- app/code/Magefine/CustomerSegment/etc/db_schema.xml -->
<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
<table name="magefine_segment" resource="default" engine="innodb" comment="Magefine Customer Segments">
<column xsi:type="int" name="segment_id" padding="10" unsigned="true" nullable="false" identity="true" />
<column xsi:type="text" name="name" nullable="false" />
<column xsi:type="text" name="code" nullable="false" />
<column xsi:type="text" name="description" nullable="true" />
<column xsi:type="smallint" name="is_active" nullable="false" default="1" />
<column xsi:type="timestamp" name="updated_at" nullable="false" default="CURRENT_TIMESTAMP" on_update="true" />
<constraint xsi:type="primary" referenceId="PRIMARY">
<column name="segment_id"/>
</constraint>
</table>
<table name="magefine_segment_condition" resource="default" engine="innodb" comment="Magefine Segment Conditions">
<column xsi:type="int" name="condition_id" padding="10" unsigned="true" nullable="false" identity="true" />
<column xsi:type="int" name="segment_id" nullable="false" unsigned="true" />
<column xsi:type="text" name="attribute_code" nullable="false" />
<column xsi:type="text" name="operator" nullable="false" />
<column xsi:type="text" name="value" nullable="false" />
<constraint xsi:type="primary" referenceId="PRIMARY">
<column name="condition_id"/>
</constraint>
<constraint xsi:type="foreign" referenceId="MAGEFINE_SEGMENT_COND_SEG">
<column name="segment_id"/>
<reference table="magefine_segment" column="segment_id"/>
<onDelete>CASCADE</onDelete>
</constraint>
</table>
<table name="magefine_segment_customer" resource="default" engine="innodb" comment="Segment customers cache">
<column xsi:type="int" name="segment_customer_id" padding="10" unsigned="true" nullable="false" identity="true" />
<column xsi:type="int" name="segment_id" nullable="false" unsigned="true" />
<column xsi:type="int" name="customer_id" nullable="false" unsigned="true" />
<column xsi:type="timestamp" name="added_at" nullable="false" default="CURRENT_TIMESTAMP" />
<constraint xsi:type="primary" referenceId="PRIMARY">
<column name="segment_customer_id"/>
</constraint>
<constraint xsi:type="unique" referenceId="UNQ_SEG_CUST">
<column name="segment_id"/>
<column name="customer_id"/>
</constraint>
</table>
</schema>
This structure stores rules in a simple condition table. For more advanced rules you could store JSON expression trees but simple rows are easier to query and maintain initially.
Integrating with existing customer data
Magento has native customer attributes (firstname, lastname, email, group_id) and address attributes (city, country_id). You should use these before creating custom fields. When you need custom attributes (loyalty_score, lifetime_value, external_id), add EAV customer attributes or use a separate table if you prefer relational fields.
To create a simple custom customer attribute for loyalty_score (example):
// app/code/Magefine/CustomerSegment/Setup/InstallData.php (if using declarative schema, use InstallData only for attributes)
public function install(ModuleDataSetupInterface $setup, ModuleContextInterface $context)
{
$eavSetup = $this->eavSetupFactory->create(['setup' => $setup]);
$eavSetup->addAttribute(
\Magento\Customer\Model\Customer::ENTITY,
'loyalty_score',
[
'type' => 'int',
'label' => 'Loyalty Score',
'input' => 'text',
'required' => false,
'visible' => true,
'user_defined' => true,
'position' => 999,
'system' => 0,
]
);
$attribute = $this->eavConfig->getAttribute('customer', 'loyalty_score');
$attribute->setData('used_in_forms', ['adminhtml_customer']);
$attribute->save();
}
Note: prefer declarative schema for table changes and use data patches for attributes in modern Magento versions (2.3.5+). But the logic above shows the idea.
Rule engine basics
Rules drive segmentation. A rule is typically a tree: conditions combined with AND/OR. We’ll implement a compact but flexible evaluator:
- Each condition row has: attribute_code, operator, value.
- A rule evaluator reads conditions for a segment and builds a PHP predicate which is executed against a customer’s data (and optionally order data).
- For performance we’ll evaluate rules in bulk using customer collections and SQL when possible, fallback to PHP for complex checks (e.g., computed lifetime value).
Supported condition types
- Simple attribute comparison: customer.attribute operator value (equals, not equals, in, not in)
- Numeric comparison: loyalty_score >= 50
- Order behavior: order_count > 3, total_spent > 1000
- Temporal conditions: last_order_date >= 2025-01-01
- Location-based: country_id = 'US', city = 'Paris'
Example evaluator: PHP pseudo-code
class Evaluator
{
public function evaluateSegment($segmentId)
{
$conditions = $this->conditionRepository->getBySegmentId($segmentId);
// Build a customer collection
$collection = $this->customerCollectionFactory->create();
foreach ($conditions as $condition) {
if ($this->isSimpleCustomerAttribute($condition->getAttributeCode())) {
// apply filter on collection using attribute
$collection->addAttributeToFilter($condition->getAttributeCode(), [$condition->getOperator() => $condition->getValue()]);
} elseif ($condition->getAttributeCode() === 'order_count') {
// join sales_order aggregate or use a subquery
$collection = $this->applyOrderCountFilter($collection, $condition);
} else {
// fallback: load customers and filter in PHP
}
}
return $collection->getAllIds();
}
}
This approach mixes SQL-friendly filters and PHP checks. For heavy traffic stores, prefer running precomputed metrics (order_count, total_spent) via cron jobs and storing them as customer attributes or in a dedicated aggregated table to make filtering fast.
Applying order-based filters in SQL
To filter by order_count, you can use a subquery joining sales_order table. A common pattern:
$collection->getSelect()->joinLeft(
['so_agg' => new \Zend_Db_Expr('(SELECT customer_id, COUNT(entity_id) as order_count, SUM(grand_total) as total_spent, MAX(created_at) as last_order_date FROM sales_order WHERE status IN ("complete","processing") GROUP BY customer_id)')],
'e.entity_id = so_agg.customer_id',
['order_count','total_spent','last_order_date']
);
$collection->getSelect()->where('so_agg.order_count > ?', 3);
Using raw SQL subqueries inside the collection select is efficient for bulk operations run via cron. Beware of DB load — run during off-peak hours or use incremental updates.
Updating the segment membership (indexer / cron)
We will maintain a materialized membership table magefine_segment_customer. Two ways to update it:
- Event-driven: react to customer update or order placement events and re-evaluate affected segments for that customer.
- Scheduled: a cron job re-evaluates all segments or only those flagged dirty.
Combining both gives best results: events mark segments as dirty and cron rebuilds caches at regular intervals.
Example cron job skeleton
// crontab.xml
<job name="magefine_segment_reindex" instance="Magefine\CustomerSegment\Cron\Reindex" method="execute">
<schedule>0 * * * *</schedule> <!-- every hour -->
</job>
// Cron class
class Reindex
{
public function execute()
{
$segments = $this->segmentRepository->getAll();
foreach ($segments as $segment) {
if (!$segment->getIsActive()) continue;
$customerIds = $this->evaluator->evaluateSegment($segment->getId());
$this->segmentCustomerResource->replaceMembers($segment->getId(), $customerIds);
}
}
}
replaceMembers should be implemented carefully: use DB transactions and delete/insert or a diff update to avoid long locks. Example approach: create a temp table, insert new membership, swap or run delete where not in new set and insert missing rows.
REST API for external marketing tools
Expose segments and their members using Magento's webapi. Define interfaces and webapi.xml routes with proper ACL and token authentication. Many marketing tools accept a webhook or API to import segments; you can push or let them pull.
API contract example
- GET /V1/magefine/segments — list segments (id, code, name, description, member_count)
- GET /V1/magefine/segments/{id}/members — list member customer data (email, name, country, custom fields)
- POST /V1/magefine/segments/{id}/export — trigger async export to a configured endpoint
<!-- etc/webapi.xml -->
<routes xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Webapi:etc/webapi.xsd">
<route url="/V1/magefine/segments" method="GET">
<service class="Magefine\CustomerSegment\Api\SegmentRepositoryInterface" method="getList"/>
<resources>
<resource ref="anonymous"/> <!-- or configurable acl -->
</resources>
</route>
<route url="/V1/magefine/segments/:segmentId/members" method="GET">
<service class="Magefine\CustomerSegment\Api\SegmentMemberInterface" method="getMembers"/>
<resources>
<resource ref="anonymous"/>
</resources>
</route>
</routes>
In the implementation for getMembers, return a compact customer DTO (email, first_name, last_name, customer_id, country, custom attributes). Keep page size in mind and implement pagination to avoid huge payloads.
Pushing segments to Mailchimp / Klaviyo
You have two integration patterns:
- Marketing tool pulls: expose a secure API and let the tool fetch members.
- Magento pushes: implement an export job which calls the marketing tool’s API to update lists/segments.
For pushing, store API keys and endpoints in the Magento admin configuration and use async jobs to avoid blocking customer-facing flows. Example export flow:
- User triggers export in admin.
- System queues an export job with segment id and target configuration.
- Worker reads queue, fetches members via segment repository, and calls external API (handle rate limits, retries).
Admin UI: segment builder and rule editor
Admin users need a clean UI to define segments and rules. Use UI components (forms) and a custom react/Knockout-based rule builder if you want a fancy drag-and-drop condition tree. For a minimal MVP, a table-based conditions editor is fine.
Key parts:
- Form to create segment (name, code, description, active).
- Grid to add conditions: each row = attribute, operator, value, group (and/or).
- Buttons: Test segment (run evaluator for a sample of customers), Reindex now, Export.
Example UI component config: a simple form and a data provider to persist JSON of conditions if you prefer to store structured JSON in a conditions column instead of many rows.
Dashboard of analysis
Segments are only useful if you measure their impact. Build a small dashboard that shows:
- Segment size (current member_count)
- Conversion rate over time (e.g., percentage of members who placed an order in last 30 days)
- Average order value (AOV) and total revenue generated by segment
- Engagement: emails sent vs opened — if you integrate with an ESP, import open/click metrics
Implementation approach:
- Store analytics snapshots (daily): segment_id, date, member_count, orders_count, revenue. A cron job computes these from sales_order and segment_customer table.
- Expose charts in admin via a small JS charting library (Chart.js). Use AJAX endpoints to get chart data.
Sample analytics snapshot cron
public function execute()
{
$segments = $this->segmentRepository->getAll();
$date = (new \DateTime())->format('Y-m-d');
foreach ($segments as $segment) {
$memberIds = $this->segmentCustomerResource->getCustomerIdsForSegment($segment->getId());
$orderStats = $this->orderStats->getForCustomerIds($memberIds); // implement a lightweight aggregator
$this->analyticsResource->saveSnapshot($segment->getId(), $date, count($memberIds), $orderStats['orders'], $orderStats['revenue']);
}
}
That snapshot table becomes the source for charts and KPI tiles in the admin UI. Keep the queries efficient — if a segment contains millions of customers, sampling or pre-aggregation is necessary.
Security and performance tips
- Protect webapi endpoints: use tokens, scopes, and review ACL settings.
- Avoid long-running web requests. Exports and heavy re-indexes should be async.
- Use batch processing: for large segments, update membership in chunks.
- Prefer storing computed metrics (order_count, total_spent) as attributes or in a summary table to make filtering fast.
- Use DB indexes on segment_customer.segment_id and customer_id for quick lookups.
Code examples: step-by-step for a minimal working flow
Let me give you a condensed step-by-step example to implement a minimal flow: create segment, add a simple condition (country_id = 'FR'), reindex, expose members via API.
1) Create segment model, resource, repository
// Model: app/code/Magefine/CustomerSegment/Model/Segment.php
namespace Magefine\CustomerSegment\Model;
use Magento\Framework\Model\AbstractModel;
class Segment extends AbstractModel
{
protected function _construct()
{
$this->_init(\Magefine\CustomerSegment\Model\ResourceModel\Segment::class);
}
}
// Resource: ResourceModel/Segment.php and ResourceModel/Segment/Collection.php standard implementations
2) Condition repository (simple)
// Repository returns condition objects with attribute_code, operator, value
$conditions = $this->conditionCollectionFactory->create()->addFieldToFilter('segment_id', $segmentId);
3) Evaluator: simple country filter
// Evaluator->evaluateSegment($segmentId)
$collection = $this->customerCollectionFactory->create();
foreach ($conditions as $c) {
if ($c->getAttributeCode() === 'country_id') {
// need to join default_billing address to get country
$collection->getSelect()->joinLeft(
['addr' => 'customer_address_entity'],
'e.default_billing = addr.entity_id',
[]
);
$collection->getSelect()->where('addr.country_id = ?', $c->getValue());
}
}
$ids = $collection->getAllIds();
4) Save members to magefine_segment_customer
// SegmentCustomerResource::replaceMembers
public function replaceMembers($segmentId, array $customerIds)
{
$connection = $this->getConnection();
$table = $this->getTable('magefine_segment_customer');
$connection->delete($table, ['segment_id = ?' => $segmentId]);
$rows = [];
foreach ($customerIds as $cid) {
$rows[] = ['segment_id' => $segmentId, 'customer_id' => $cid, 'added_at' => (new \DateTime())->format('Y-m-d H:i:s')];
}
if (!empty($rows)) {
$connection->insertMultiple($table, $rows);
}
}
5) Expose members in webapi
// Api/SegmentMemberInterface.php
/**
* @return \Magefine\CustomerSegment\Api\Data\MemberSearchResultsInterface
*/
public function getMembers($segmentId, $searchCriteria = null);
// Implementation loads magefine_segment_customer by segment id, then loads customers and returns DTOs
Practical tips when testing
- Start with small data sets locally and add unit/integration tests for evaluator logic.
- Log SQL queries for a sample evaluation to check for unexpected full-table scans.
- Test event-driven updates (e.g., new order) and ensure segment membership changes are reflected after cron runs.
- Use Magento's built-in cache and invalidate relevant keys when segments change to keep dashboard fast.
Scaling considerations
If your store has millions of customers, the naive approach will fail. Consider:
- Pre-aggregation: run nightly jobs to compute customer metrics used in conditions.
- Sharding or partitioning the segment_customer table if necessary.
- Using read replicas for heavy SELECT loads (e.g., exports, analytics queries).
- Caching API responses for frequently-requested segments.
Example: Use case scenarios
Here are a few practical segments you may implement and how to represent them in conditions:
- Recent high-value customers: total_spent >= 500 && last_order_date >= 2025-06-01
- At-risk customers: last_order_date <= 2024-10-01 && order_count <= 1
- Local event invite: country_id = 'FR' && city = 'Lyon'
- VIP group: customer_group_id = 3 && loyalty_score >= 80
For each one, balance accuracy and performance. If a rule requires computing expensive aggregates, compute them ahead of time and store as attributes.
Deploying and maintaining
- Ship as a module via composer package if you maintain multiple stores.
- Document the supported operators and attributes so marketing users know limits.
- Monitor long-running reindex jobs and add observability (logs, metrics).
- Provide administrators with an "Estimate segment size" tool so they can see membership counts before triggering large exports.
Final thoughts
Building a custom customer segmentation module in Magento 2 is absolutely doable and gives you full control over how segments are defined, evaluated, and exported. The heart of the system is a good DB design, a pragmatic rule evaluator that mixes SQL and PHP depending on complexity, and a simple, safe API to let marketing systems consume segment data. Add a small analytics layer and you’ll be able to monitor performance and iterate on your campaigns.
If you want, I can:
- Provide a complete skeleton module zip with files and db_schema.xml ready to install.
- Design a sample UI component for the rule editor using Knockout + UI components.
- Share example integrations for Mailchimp and Klaviyo with sample export worker code (keeping keys and secrets secure).
Tell me which of those you'd like first and I’ll generate the concrete files.
Thanks for reading — this approach should be a solid starting point for a Magefine-ready segmentation module that scales and integrates with the tools your marketing team already uses.