diff --git a/doc/ref/makedocreldata.g b/doc/ref/makedocreldata.g
index e19999cc400..e1f77aabde5 100644
--- a/doc/ref/makedocreldata.g
+++ b/doc/ref/makedocreldata.g
@@ -148,6 +148,8 @@ GAPInfo.ManualDataRef:= rec(
"../../lib/pquot.gd",
"../../lib/primality.gd",
"../../lib/process.gd",
+ "../../lib/productdomain.gd",
+ "../../lib/productdomain.gi",
"../../lib/profile.g",
"../../lib/proto.gd",
"../../lib/randiso.gd",
diff --git a/lib/productdomain.gd b/lib/productdomain.gd
new file mode 100644
index 00000000000..50e1947a4bc
--- /dev/null
+++ b/lib/productdomain.gd
@@ -0,0 +1,51 @@
+#############################################################################
+##
+## This file declares everything we need to work with IsDirectProductDomain
+## objects.
+##
+## <#GAPDoc Label="DirectProductFamily">
+##
+##
+##
+##
+## args must be a dense list of
+## families, otherwise the function raises an error.
+##
+## returns a collections family fam
+## with the following property:
+## Each collection coll in fam is a direct product
+## whose i-th factors are collections in args[i].
+## This is modelled on the level of the elements by requiring that each
+## elm of coll must be an
+## object with elm[i] contained in ElementsFamily(args[i]).
+##
+## Note though that not all direct products in &GAP; are created via these
+## families, see for example for permutation
+## groups.
+##
+## D8 := DihedralGroup(IsPermGroup, 8);;
+## gap> fam := FamilyObj(D8);
+##
+## gap> ElementsFamily(fam);
+##
+## gap> productFamily := DirectProductFamily([fam, fam]);
+##
+## gap> elmsOfProductFamily := ElementsFamily(productFamily);
+## > )">
+## gap> ComponentsOfDirectProductElementsFamily(elmsOfProductFamily);
+## [ , ]
+## ]]>
+##
+##
+## <#/GAPDoc>
+DeclareGlobalFunction( "DirectProductFamily",
+ "for a dense list of collection families" );
+
+DeclareCategory("IsDirectProductDomain",
+ IsDirectProductElementCollection and IsDomain);
+
+DeclareOperation("DirectProductDomain", [IsDenseList]);
+
+DeclareAttribute("ComponentsOfDirectProductDomain", IsDirectProductDomain);
+DeclareAttribute("DimensionOfDirectProductDomain", IsDirectProductDomain);
diff --git a/lib/productdomain.gi b/lib/productdomain.gi
new file mode 100644
index 00000000000..87b6a6d05b0
--- /dev/null
+++ b/lib/productdomain.gi
@@ -0,0 +1,101 @@
+#############################################################################
+##
+## The rest of this file implements the operations for IsDirectProductDomain
+## domains.
+##
+InstallGlobalFunction(DirectProductFamily,
+function(args)
+ if not IsDenseList(args) or not ForAll(args, IsCollectionFamily) then
+ ErrorNoReturn(" must be a dense list of collection families");
+ fi;
+ return CollectionsFamily(
+ DirectProductElementsFamily(List(args, ElementsFamily))
+ );
+end);
+
+
+#############################################################################
+##
+InstallMethod(DirectProductDomain,
+"for a dense list (of domains)",
+[IsDenseList],
+function(args)
+ local directProductFamily, type;
+ if not ForAll(args, IsDomain) then
+ ErrorNoReturn("args must be a dense list of domains");
+ fi;
+ directProductFamily := DirectProductFamily(List(args, FamilyObj));
+ type := NewType(directProductFamily,
+ IsDirectProductDomain and IsAttributeStoringRep);
+ return ObjectifyWithAttributes(rec(), type,
+ ComponentsOfDirectProductDomain, args);
+end);
+
+InstallOtherMethod(DirectProductDomain,
+"for a domain and a nonnegative integer",
+[IsDomain, IsInt],
+function(dom, k)
+ local directProductFamily, type;
+ if k < 0 then
+ ErrorNoReturn(" must be a nonnegative integer");
+ fi;
+ directProductFamily := DirectProductFamily(
+ ListWithIdenticalEntries(k, FamilyObj(dom))
+ );
+ type := NewType(directProductFamily,
+ IsDirectProductDomain and IsAttributeStoringRep);
+ return ObjectifyWithAttributes(rec(),
+ type,
+ ComponentsOfDirectProductDomain,
+ ListWithIdenticalEntries(k, dom));
+end);
+
+InstallMethod(PrintObj,
+"for an IsDirectProductDomain",
+[IsDirectProductDomain],
+function(dom)
+ local components, i;
+ Print("DirectProductDomain([ ");
+ components := ComponentsOfDirectProductDomain(dom);
+ for i in [1 .. Length(components)] do
+ PrintObj(components[i]);
+ if i < Length(components) then
+ Print(", ");
+ fi;
+ od;
+ Print(" ])");
+end);
+
+InstallMethod(Size,
+"for an IsDirectProductDomain",
+[IsDirectProductDomain],
+function(dom)
+ local size, comp;
+ size := 1;
+ for comp in ComponentsOfDirectProductDomain(dom) do
+ size := Size(comp) * size;
+ od;
+ return size;
+end);
+
+InstallMethod(DimensionOfDirectProductDomain,
+"for an IsDirectProductDomain",
+[IsDirectProductDomain],
+dom -> Length(ComponentsOfDirectProductDomain(dom)));
+
+InstallMethod(\in,
+"for an IsDirectProductDomain",
+[IsDirectProductElement, IsDirectProductDomain],
+function(elm, dom)
+ local components, i;
+ if Length(elm) <> DimensionOfDirectProductDomain(dom) then
+ return false;
+ fi;
+ components := ComponentsOfDirectProductDomain(dom);
+ for i in [1 .. Length(components)] do
+ if not elm[i] in components[i] then
+ return false;
+ fi;
+ od;
+ return true;
+end);
diff --git a/lib/read3.g b/lib/read3.g
index 6cc9804b8a7..436d3eb2fa5 100644
--- a/lib/read3.g
+++ b/lib/read3.g
@@ -27,6 +27,7 @@ ReadLib( "bitfields.gd" );
ReadLib( "mapping.gd" );
ReadLib( "mapphomo.gd" );
ReadLib( "relation.gd");
+ReadLib( "productdomain.gd" );
ReadLib( "magma.gd" );
ReadLib( "mgmideal.gd" );
diff --git a/lib/read5.g b/lib/read5.g
index e36369c75bd..581aed4b228 100644
--- a/lib/read5.g
+++ b/lib/read5.g
@@ -28,6 +28,7 @@ ReadLib( "mapping.gi" );
ReadLib( "mapprep.gi" );
ReadLib( "mapphomo.gi" );
ReadLib( "relation.gi" );
+ReadLib( "productdomain.gi" );
ReadLib( "magma.gi" );
ReadLib( "mgmideal.gi" );
diff --git a/lib/tuples.gd b/lib/tuples.gd
index 09172b5faf3..0982901bf5d 100644
--- a/lib/tuples.gd
+++ b/lib/tuples.gd
@@ -178,46 +178,3 @@ direct product elements families" );
DeclareOperation( "DirectProductElement", [ IsList ]);
DeclareOperation( "DirectProductElementNC",
[ IsDirectProductElementFamily, IsList ]);
-
-
-#############################################################################
-##
-##
-## <#GAPDoc Label="DirectProductFamily">
-##
-##
-##
-##
-## args must be a dense list of
-## families, otherwise the function raises an error.
-##
-## returns a collections family fam
-## with the following property:
-## Each collection coll in fam is a direct product
-## whose i-th factors are collections in args[i].
-## This is modelled on the level of the elements by requiring that each
-## elm of coll must be an
-## object with elm[i] contained in ElementsFamily(args[i]).
-##
-## Note though that not all direct products in &GAP; are created via these
-## families, see for example for permutation
-## groups.
-##
-## D8 := DihedralGroup(IsPermGroup, 8);;
-## gap> fam := FamilyObj(D8);
-##
-## gap> ElementsFamily(fam);
-##
-## gap> productFamily := DirectProductFamily([fam, fam]);
-##
-## gap> elmsOfProductFamily := ElementsFamily(productFamily);
-## > )">
-## gap> ComponentsOfDirectProductElementsFamily(elmsOfProductFamily);
-## [ , ]
-## ]]>
-##
-##
-## <#/GAPDoc>
-DeclareGlobalFunction( "DirectProductFamily",
- "for a dense list of collection families" );
diff --git a/lib/tuples.gi b/lib/tuples.gi
index a4bb72148fd..15bd75a44ac 100644
--- a/lib/tuples.gi
+++ b/lib/tuples.gi
@@ -506,17 +506,3 @@ InstallOtherMethod( \*,
fi;
return DirectProductElement( List( dpelm, entry -> nonlist * entry ) );
end );
-
-
-#############################################################################
-##
-##
-InstallGlobalFunction( DirectProductFamily,
- function( args )
- if not IsDenseList(args) or not ForAll(args, IsCollectionFamily) then
- ErrorNoReturn(" must be a dense list of collection families");
- fi;
- return CollectionsFamily(
- DirectProductElementsFamily( List( args, ElementsFamily ) )
- );
- end );
diff --git a/tst/testinstall/productdomain.tst b/tst/testinstall/productdomain.tst
new file mode 100644
index 00000000000..bfac1b79247
--- /dev/null
+++ b/tst/testinstall/productdomain.tst
@@ -0,0 +1,105 @@
+#@local D8, fam, dpf, d, emptyDPDDim2, emptyDPDDim3, dpdDim0, dpd
+#@local range1, range2, g1, g2, dpdOfGroups, bijToRange, inv, tups
+#@local dpdNotAttributeStoring
+gap> START_TEST("productdomain.tst");
+
+# DirectProductFamily
+gap> D8 := DihedralGroup(IsPermGroup, 8);;
+gap> fam := FamilyObj(D8);
+
+gap> ElementsFamily(fam);
+
+gap> dpf := DirectProductFamily([fam, fam]);
+
+gap> IsDirectProductElementFamily(ElementsFamily(dpf));
+true
+gap> DirectProductFamily([CyclotomicsFamily, ]);
+Error, must be a dense list of collection families
+
+# DirectProductDomain
+# of empty domains, dim 2
+gap> d := Domain(FamilyObj([1]), []);
+Domain([ ])
+gap> emptyDPDDim2 := DirectProductDomain([d, d]);
+DirectProductDomain([ Domain([ ]), Domain([ ]) ])
+gap> Size(emptyDPDDim2);
+0
+gap> IsEmpty(emptyDPDDim2);
+true
+gap> DimensionOfDirectProductDomain(emptyDPDDim2);
+2
+gap> DirectProductElement([]) in emptyDPDDim2;
+false
+
+# of empty domains, dim 3
+gap> emptyDPDDim3 := DirectProductDomain(d, 3);
+DirectProductDomain([ Domain([ ]), Domain([ ]), Domain([ ]) ])
+gap> Size(emptyDPDDim3);
+0
+gap> IsEmpty(emptyDPDDim3);
+true
+gap> DimensionOfDirectProductDomain(emptyDPDDim3);
+3
+gap> DirectProductElement([]) in emptyDPDDim3;
+false
+
+# of dimension 0
+gap> range1 := Domain([1..5]);
+Domain([ 1 .. 5 ])
+gap> dpdDim0 := DirectProductDomain(range1, 0);
+DirectProductDomain([ ])
+gap> Size(dpdDim0);
+1
+gap> IsEmpty(dpdDim0);
+false
+gap> DimensionOfDirectProductDomain(dpdDim0);
+0
+gap> DirectProductElement([]) in dpdDim0;
+true
+
+# of domains of ranges
+gap> range1;
+Domain([ 1 .. 5 ])
+gap> range2 := Domain([3..7]);
+Domain([ 3 .. 7 ])
+gap> dpd := DirectProductDomain([range1, range2]);
+DirectProductDomain([ Domain([ 1 .. 5 ]), Domain([ 3 .. 7 ]) ])
+gap> Size(dpd);
+25
+gap> DimensionOfDirectProductDomain(dpd);
+2
+gap> DirectProductElement([]) in dpd;
+false
+gap> DirectProductElement([6, 3]) in dpd;
+false
+gap> DirectProductElement([1, 3]) in dpd;
+true
+
+# DirectProductDomain
+# of groups
+gap> g1 := DihedralGroup(4);
+
+gap> g2 := DihedralGroup(IsPermGroup, 4);
+Group([ (1,2), (3,4) ])
+gap> dpdOfGroups := DirectProductDomain([g1, g2]);
+DirectProductDomain([ Group( [ f1, f2 ] ), Group( [ (1,2), (3,4) ] ) ])
+gap> Size(dpdOfGroups);
+16
+gap> DimensionOfDirectProductDomain(dpdOfGroups);
+2
+gap> DirectProductElement([]) in dpdOfGroups;
+false
+gap> DirectProductElement([1, 3]) in dpdOfGroups;
+false
+gap> DirectProductElement([g1.1, g2.1]) in dpdOfGroups;
+true
+
+# DirectProductDomain
+# error handling
+gap> DirectProductDomain([CyclotomicsFamily]);
+Error, args must be a dense list of domains
+gap> DirectProductDomain(dpd, -1);
+Error, must be a nonnegative integer
+
+#
+gap> STOP_TEST("productdomain.tst");
diff --git a/tst/testinstall/tuples.tst b/tst/testinstall/tuples.tst
deleted file mode 100644
index a1f0fe18bc5..00000000000
--- a/tst/testinstall/tuples.tst
+++ /dev/null
@@ -1,13 +0,0 @@
-gap> START_TEST("tuples.tst");
-gap> D8 := DihedralGroup(IsPermGroup, 8);;
-gap> fam := FamilyObj(D8);
-
-gap> ElementsFamily(fam);
-
-gap> dpf := DirectProductFamily([fam, fam]);
-
-gap> IsDirectProductElementFamily(ElementsFamily(dpf));
-true
-
-#
-gap> STOP_TEST("tuples.tst");