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, étape-by-étape implémentation: module skeleton, database architecture, comment integrate native and custom attribut clients, a flexible rule engine to define segments by purchase behavior or location, a API REST so external marketing tools (Mailchimp, Klaviyo, etc.) can consume segments, and a simple analytics tableau de bord to measure performance.

Why build a custom segmentation module?

Magento 2 has attribut clients and marketing tools, but a custom segmentation module vous donne full control to:

  • Create reusable segments using entreprise rules (e.g., "clients who bought X in last 90 days").
  • Use native and attributs personnalisés (city, group, custom loyalty score).
  • Expose segments via a API REST to feed external tools like Klaviyo or Mailchimp.
  • Measure segment performance via an integrated tableau de bord.

It’s especially useful for stores that want tighter control than a tiers SaaS solution or want to keep segmentation logic close to data to avoid sync problèmes.

High-level architecture

Let’s design a clear architecture so the code stays maintainable:

  • Database entities: segment, rule, rule_condition, segment_client (materialized membership cache).
  • Rule engine: interprets rules and evaluates them against client data (attributes, commande history, location).
  • Updater / indexeur: re-evaluates segments periodically or on events to update segment_client table.
  • API REST: read-only endpoints to export segments and members.
  • Admin UI: create/edit segments and rules, and a tableau de bord 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>

Nous allons 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 table de base de donnéess well keeps things fast and predictable. Use Magento's db_schema.xml (recommended over install/mise à jour 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, commande_count > 3)
  • magefine_segment_client: materialized table of client_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 lignes are easier to query and maintain initially.

Integnote with existing client data

Magento has native attribut clients (firstname, lastname, e-mail, group_id) and address attributes (city, country_id). Vous devriez use these before creating custom champs. Quand vous need attributs personnalisés (loyalty_score, lifetime_valeur, external_id), add EAV attribut clients or use a separate table if you prefer relational champs.

To create a simple custom attribut client for loyalty_score (exemple):

// 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 correctifs 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 ligne has: attribute_code, operator, valeur.
  • A rule evaluator reads conditions for a segment and builds a PHP predicate which is executed against a client’s data (and optionally commande data).
  • For performance we’ll evaluate rules in bulk using client collections and SQL when possible, fallback to PHP for complex checks (e.g., computed lifetime valeur).

Supported condition types

  • Simple attribute comparison: client.attribute operator valeur (equals, not equals, in, not in)
  • Numeric comparison: loyalty_score >= 50
  • Order behavior: commande_count > 3, total_spent > 1000
  • Temporal conditions: last_commande_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();
    }
}

Cette approche mixes SQL-friendly filtres and PHP checks. For heavy traffic stores, prefer running precomputed metrics (commande_count, total_spent) via tâches cron and storing them as attribut clients or in a dedicated aggregated table to make filtreing fast.

Applying commande-based filtres in SQL

To filtre by commande_count, you can use a subquery joining sales_commande 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 (indexeur / cron)

Nous allons maintain a materialized membership table magefine_segment_client. Two ways to update it:

  • Event-driven: react to client update or commande placement events and re-evaluate affected segments for that client.
  • Scheduled: a tâche cron 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 tâche cron 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 devrait être implemented carefully: use DB transactions and delete/insert or a diff update to avoid long locks. Example approche: create a temp table, insert new membership, swap or run delete where not in new set and insert missing lignes.

API REST 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 exemple

  • GET /V1/magefine/segments — list segments (id, code, name, description, member_count)
  • GET /V1/magefine/segments/{id}/members — list member client data (e-mail, name, country, custom champs)
  • 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 implémentation for getMembers, return a compact client DTO (e-mail, first_name, last_name, client_id, country, attributs personnalisés). Keep page size in mind and implement pagination to avoid huge payloads.

Pushing segments to Mailchimp / Klaviyo

You have two integration patterns:

  1. Marketing tool pulls: expose a secure API and let the tool fetch members.
  2. Magento pushes: implement an export job which calls the marketing tool’s API to update lists/segments.

For pushing, store API clés and endpoints in the Magento configuration d'administration and use async jobs to avoid blocking client-facing flows. Example export flow:

  1. User triggers export in admin.
  2. System queues an export job with segment id and target configuration.
  3. Worker reads queue, fetches members via segment repository, and calls external API (handle rate limits, retries).

Admin UI: segment builder and rule editor

Admin utilisateurs need a clean UI to define segments and rules. Use UI composants (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 ligne = attribute, operator, valeur, group (and/or).
  • Buttons: Test segment (run evaluator for a sample of clients), Reindex now, Export.

Example UI composant config: a simple form and a data provider to persist JSON of conditions if you prefer to store structured JSON in a conditions colonne au lieu de many lignes.

Dashboard of analysis

Segments are only useful if you measure their impact. Build a small tableau de bord that shows:

  • Segment size (current member_count)
  • Conversion rate over time (e.g., percentage of members who placed an commande in last 30 days)
  • Average commande valeur (AOV) and total revenue generated by segment
  • Engagement: e-mails sent vs opened — if you integrate with an ESP, import open/click metrics

Implementation approche:

  1. Store analytics snapshots (daily): segment_id, date, member_count, commandes_count, revenue. A tâche cron computes these from sales_commande and segment_client table.
  2. 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 clients, sampling or pre-aggregation est nécessaire.

Security and performance conseils

  • Protect webapi endpoints: use tokens, scopes, and avis ACL settings.
  • Avoid long-running web requests. Exports and heavy re-indexes devrait être async.
  • Use batch processing: for large segments, update membership in chunks.
  • Prefer storing computed metrics (commande_count, total_spent) as attributes or in a résumé table to make filtreing fast.
  • Use DB indexes on segment_client.segment_id and client_id for quick lookups.

Code exemples: étape-by-étape for a minimal working flow

Let me give you a condensed étape-by-étape exemple to implement a minimal flow: create segment, add a simple condition (country_id = 'FR'), réindexer, 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 filtre

// 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_client

// 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 conseils when test

  • Start with small data sets locally and add unit/test d'intégrations for evaluator logic.
  • Log SQL queries for a sample evaluation to check for unexpected full-table scans.
  • Test event-driven updates (e.g., new commande) and ensure segment membership changes are reflected after cron runs.
  • Use Magento's built-in cache and invalidate relevant clés when segments change to keep tableau de bord fast.

Scaling considerations

If your store has millions of clients, the naive approche will fail. Consider:

  • Pre-aggregation: run nightly jobs to compute client metrics used in conditions.
  • Sharding or partitioning the segment_client 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

Voici a few practical segments you may implement and comment represent them in conditions:

  • Recent high-valeur clients: total_spent >= 500 && last_commande_date >= 2025-06-01
  • At-risk clients: last_commande_date <= 2024-10-01 && commande_count <= 1
  • Local event invite: country_id = 'FR' && city = 'Lyon'
  • VIP group: client_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 mulconseille stores.
  • Document the supported operators and attributes so marketing utilisateurs know limits.
  • Monitor long-running réindexer 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 client segmentation module in Magento 2 is absolutely doable and vous donne 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.

Si vous want, I can:

  • Provide a complete skeleton module zip with fichiers and db_schema.xml ready to install.
  • Design a sample UI composant for the rule editor using Knockout + UI composants.
  • Share exemple integrations for Mailchimp and Klaviyo with sample export worker code (keeping clés and secrets secure).

Tell me which of those you'd like first and I’ll generate the concrete fichiers.

Thanks for reading — this approche devrait être a solid starting point for a Magefine-ready segmentation module that scales and integrates with the tools your marketing team already uses.